Download Android Development 2

Document related concepts
no text concepts found
Transcript
Android Development 2
Lesso n 1: Fragm e nt s
The Sandbo x Enviro nment
Abo ut Eclipse
Perspectives and the Red Leaf Ico n
Wo rking Sets
Andro id Fragments
Using Fragments Pro gramatically
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 2: Lo ade rs
Why Use a Lo ader?
Perfo rming Tasks in a Lo ader
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 3: Advance d Layo ut s
Suppo rting Orientatio n Changes
Persisting Data o n Ro tatio n
Suppo rting Multiple Screen Sizes
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 4: Cust o m Vie w Co m po ne nt s
Defining a Custo m Co mpo nent
Implementing View Attributes in a Custo m Co mpo nent
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 5: Basic Se rvice s
Creating, Declaring, and Starting a Service
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 6 : No t if icat io ns
Creat and Update a No tificatio n
Respo nding To User Taps On A No tificatio n
Updating A No tificatio n
Wrapping Up
Quiz 1 Pro ject 1 Pro ject 2
Lesso n 7: Co nt e nt Pro vide rs
Creating and Using a Co ntent Pro vider
Examining the Co de
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 8 : Cam e ra Basics: Using t he Built -in Cam e ra Applicat io n
Starting the Built-in Camera Using an Intent
Saving Image to External Sto rage
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 9 : Cam e ra Advance d: Building a Cust o m Cam e ra Applicat io n
Using the Camera API
Camera Parameters
Checking fo r a Camera and Handling Multiple Cameras
Camera Features and the Andro id Manifest
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 10 : Bro adcast Re ce ive rs
Creating a Bro adcastReceiver fo r System Events
Creating a Bro adcastReceiver fo r Service Events
Using the Lo calBro adcastManager
Wrapping Up
Quiz 1 Pro ject 1
Lesso n 11: Me dia: Audio
Creating a MediaPlayer and Playing an Audio File
Handling MediaPlayer State and the Activity Lifecycle
Handling MediaPlayer Events and UI Updates
Wrapping Up Audio
Quiz 1 Pro ject 1
Lesso n 12: Me dia: Vide o
Video Playback with a Video View
Adding a MediaCo ntro ller to a Video View
Video View Events and Metho ds
Wrapping UP
Quiz 1 Pro ject 1
Lesso n 13: We bVie w
WebView Basics
Using WebSettings
Using a WebChro meClient
Using WebViewClient
Using WebView Metho ds
Enabling JavaScript
WebView Wrap-up
Quiz 1 Quiz 2 Pro ject 1
Lesso n 14: Andro id 2 Final Pro je ct
Final Pro ject
Pro ject 1
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Fragments
Welco me to the O'Reilly Scho o l o f Techno lo gy Andro id 2 co urse!
Course Objectives
When yo u co mplete this co urse, yo u will be able to :
create applicatio ns o ptimized fo r bo th pho nes and tablets.
suppo rt o ld and new devices using the Andro id suppo rt library.
utilize vario us Andro id systems fo r sharing with and receiving data fro m o ther Andro id applicatio ns.
create media rich applicatio ns with audio and video .
Note
If yo u're new to Andro id, we highly reco mmend that yo u co ntact us to co mplete the first Andro id co urse befo re
taking this o ne. The lesso ns in this Andro id 2 co urse will all assume yo u have a firm grasp o n Object Oriented
Pro gramming, the Java pro gramming language, and the basics o f Andro id applicatio n develo pment with the
Andro id SDK.
If yo u've already co mpleted the first Andro id co urse in this series o r are already familiar with using Eclipse under
the remo te develo pment pro cess fo r O'Reilly Scho o l o f Techno lo gy, yo u can skip ahead to the Andro id
Fragments sectio n.
Lesson Objectives
When yo u co mplete this lesso n, yo u will be able to :
learn abo ut the UserActive metho d o f learning.
read Abo ut the Learning Sandbo x Enviro nment.
set Up Eclipse fo r Wo rking with Andro id Applicatio ns.
create A Simple Website.
add Web Co ntro ls to Yo ur Website.
Learning with O'Reilly School of T echnology Courses
As with every O'Reilly Scho o l o f Techno lo gy co urse, we'll take a user-active appro ach to learning. This means that yo u
(the user) will be active! Yo u'll learn by do ing, building live pro grams, testing them and experimenting with them—
hands-o n!
To learn a new skill o r techno lo gy, yo u have to experiment. The mo re yo u experiment, the mo re yo u learn. Our system
is designed to maximize experimentatio n and help yo u learn to learn a new skill.
We'll pro gram as much as po ssible to be sure that the principles sink in and stay with yo u.
Each time we discuss a new co ncept, yo u'll put it into co de and see what YOU can do with it. On o ccasio n we'll even
give yo u co de that do esn't wo rk, so yo u can see co mmo n mistakes and ho w to reco ver fro m them. Making mistakes
is actually ano ther go o d way to learn.
Abo ve all, we want to help yo u to learn to learn. We give yo u the to o ls to take co ntro l o f yo ur o wn learning experience.
When yo u co mplete an OST co urse, yo u kno w the subject matter, and yo u kno w ho w to expand yo ur kno wledge, so
yo u can handle changes like so ftware and o perating system updates.
Here are so me tips fo r using O'Reilly Scho o l o f Techno lo gy co urses effectively:
T ype t he co de . Resist the temptatio n to cut and paste the example co de we give yo u. Typing the co de
actually gives yo u a feel fo r the pro gramming task. Then play aro und with the examples to find o ut what else
yo u can make them do , and to check yo ur understanding. It's highly unlikely yo u'll break anything by
experimentatio n. If yo u do break so mething, that's an indicatio n to us that we need to impro ve o ur system!
T ake yo ur t im e . Learning takes time. Rushing can have negative effects o n yo ur pro gress. Slo w do wn and
let yo ur brain abso rb the new info rmatio n tho ro ughly. Taking yo ur time helps to maintain a relaxed, po sitive
let yo ur brain abso rb the new info rmatio n tho ro ughly. Taking yo ur time helps to maintain a relaxed, po sitive
appro ach. It also gives yo u the chance to try new things and learn mo re than yo u o therwise wo uld if yo u
blew thro ugh all o f the co ursewo rk to o quickly.
Expe rim e nt . Wander fro m the path o ften and explo re the po ssibilities. We can't anticipate all o f yo ur
questio ns and ideas, so it's up to yo u to experiment and create o n yo ur o wn. Yo ur instructo r will help if yo u
go co mpletely o ff the rails.
Acce pt guidance , but do n't de pe nd o n it . Try to so lve pro blems o n yo ur o wn. Go ing fro m
misunderstanding to understanding is the best way to acquire a new skill. Part o f what yo u're learning is
pro blem so lving. Of co urse, yo u can always co ntact yo ur instructo r fo r hints when yo u need them.
Use all available re so urce s! In real-life pro blem-so lving, yo u aren't bo und by false limitatio ns; in OST
co urses, yo u are free to use any reso urces at yo ur dispo sal to so lve pro blems yo u enco unter: the Internet,
reference bo o ks, and o nline help are all fair game.
Have f un! Relax, keep practicing, and do n't be afraid to make mistakes! Yo ur instructo r will keep yo u at it
until yo u've mastered the skill. We want yo u to get that satisfied, "I'm so co o l! I did it!" feeling. And yo u'll have
so me pro jects to sho w o ff when yo u're do ne.
Lesson Format
We'll try o ut lo ts o f examples in each lesso n. We'll have yo u write co de, lo o k at co de, and edit existing co de. The co de
will be presented in bo xes that will indicate what needs to be do ne to the co de inside.
Whenever yo u see white bo xes like the o ne belo w, yo u'll type the co ntents into the edito r windo w to try the example
yo urself. The CODE TO TYPE bar o n to p o f the white bo x co ntains directio ns fo r yo u to fo llo w:
CODE TO TYPE:
White boxes like this contain code for you to try out (type into a file to run).
If you have already written some of the code, new code for you to add looks like this.
If we want you to remove existing code, the code to remove will look like this.
We may also include instructive comments that you don't need to type.
We may run pro grams and do so me o ther activities in a terminal sessio n in the o perating system o r o ther co mmandline enviro nment. These will be sho wn like this:
INTERACTIVE SESSION:
The plain black text that we present in these INTERACTIVE boxes is
provided by the system (not for you to type). The commands we want you to type look lik
e this.
Co de and info rmatio n presented in a gray OBSERVE bo x is fo r yo u to inspect and absorb. This info rmatio n is o ften
co lo r-co ded, and fo llo wed by text explaining the co de in detail:
OBSERVE:
Gray "Observe" boxes like this contain information (usually code specifics) for you to
observe.
The paragraph(s) that fo llo w may pro vide additio n details o n inf o rm at io n that was highlighted in the Observe bo x.
We'll also set especially pertinent info rmatio n apart in "No te" bo xes:
Note
T ip
No tes pro vide info rmatio n that is useful, but no t abso lutely necessary fo r perfo rming the tasks at hand.
Tips pro vide info rmatio n that might help make the to o ls easier fo r yo u to use, such as sho rtcut keys.
WARNING
Warnings pro vide info rmatio n that can help prevent pro gram crashes and data lo ss.
T he Sandbox Environment
About Eclipse
We're using an Integrated Develo pment Enviro nment (IDE) called Eclipse. It's the pro gram filling up yo ur
screen right no w. IDEs assist pro grammers by perfo rming many o f the tasks that need to be do ne repetitively.
IDEs can also help to edit and debug co de, and o rganize pro jects.
Note
Yo u'll make so me changes to yo ur wo rking enviro nment during this lesso n, so when yo u
co mplete the lesso n, yo u'll need to exit Eclipse to save tho se changes.
The Eclipse windo w displays lesso n co ntent, and pro vides space fo r yo u to create, manage, and run
pro grams:
Perspectives and the Red Leaf Icon
The Ellipse Plug-in fo r Eclipse, develo ped by the O'Reilly Scho o l o f Techno lo gy, adds an ico n to the to o l bar
in Eclipse. This ico n is yo ur "panic butto n." Since Eclipse is so versatile, yo u are allo wed to mo ve things
aro und, like views, to o lbars, and such. If yo u beco me co nfused and want to return to the default perspective
(windo w layo ut), clicking o n the Red Leaf ico n allo ws yo u to do that right away.
The
ico n has these functio ns:
To reset the current perspective, click the ico n.
To change perspectives, click the dro p-do wn arro w beside the ico n and select a series name
(Andro id, Java, Pytho n, C++, etc.). Mo st o f the perspectives lo o k similar, but subtle changes may
be present "behind the scenes," so it's best to use the co rrect perspective fo r the co urse. Fo r this
co urse, select Andro id.
Working Sets
All pro jects created in Eclipse exist in the wo rkspace directo ry o f yo ur acco unt o n o ur server. As yo u create
multiple pro jects fo r each lesso n in each co urse, it's po ssible that yo ur wo rkspace directo ry can beco me
pretty cluttered. To help alleviate the po tential clutter, in this co urse, we use working sets. A wo rking set is a
lo gical view o f the wo rkspace; it behaves like a fo lder, but it's really just an asso ciatio n o f files. Wo rking sets
allo w yo u to limit the detail that yo u see at any given time. The difference between a wo rking set and a fo lder is
that a wo rking set do esn't actually exist in the file system. A wo rking set is a co nvenient way to gro up related
items to gether. Yo u can assign a pro ject to o ne o r mo re wo rking sets. In so me cases, like with the Andro id
ADT plugin to Eclipse, new pro jects are created witho ut regard fo r wo rking sets and will be placed in the
wo rkspace, but no t assigned to a wo rking set (appearing in the "Other Pro jects" wo rking set). To assign o ne
o f these pro jects to a wo rking set, right-click o n the pro ject name and select Assign Wo rking Se t s fro m the
co ntext menu.
We've created so me wo rking sets fo r yo u already. To turn the wo rking set display o n and o ff in Eclipse, see
these instructio ns.
Setting Up Your Android Emulator
The Andro id team has made an excellent Eclipse plugin fo r Andro id called ADT (Andro id Develo per To o lkit). ADT helps
with Andro id develo pment in Eclipse in many different ways, so it's impo rtant that we get the Eclipse enviro nment and
ADT set up co rrectly fro m the start, so we can build and test o ur Andro id applicatio ns.
Note
The Andro id Develo per To o lkit plugin fo r Eclipse changes extremely frequently. The develo pers behind
the to o lkit are do ing amazing wo rk and co nstantly updating and impro ving the plugin. Ho wever, this
means the mo st recent versio n may differ fro m what yo u see here and what the instructio ns detail. Do n't
wo rry if what yo u see slightly differs fro m the instructio ns. While the lo o k, feel, and features may have
changed (likely fo r the better), the co re decisio ns and o ptio ns such as applicatio n and package names
will generaly still be reco gnizable. We perio dically update the to o lkit o n o ur systems.
Point ADT to the Android SDK
The ADT plugin is installed o n the instance o f Eclipse that yo u are using right no w. To o pen ADT, yo u can
either click the Andro id Virtual Device Manager ico n in the butto n bar at the to p, o r select Windo w | AVD
Manage r:
Go ahead and try that no w. Yo u'll pro bably get an erro r message info rming yo u that the Andro id SDK co uld
no t be fo und:
To fix this erro r, o pen the Eclipse preferences fro m the to o lbar menu by clicking Windo w | Pre f e re nce s. The
Eclipse preferences windo w will appear. Then click the Andro id sectio n o n the left. (Yo u may be asked if yo u
want to send usage data to Go o gle. Click "No .") Then, in the SDK Lo catio n field, type C:\Pro gram File s
(x86 )\Andro id\andro id-sdk and click OK.
Note
So metimes when reo pening a remo te Eclipse sessio n, ADT will fo rget that it already has the
lo catio n o f the SDK, and will po p-up the erro r again. If that happens, just o pen the Eclipse
Preferences windo w again (Windo w | Pre f e re nce s) and it sho uld sho w that the path is in there
already. Click OK and everything sho uld wo rk fine again.
Yo ur Preferences fo r Andro id will lo o k like this:
No w ADT is ready to go ! To test to make sure it's wo rking, o pen the ADT windo w by clicking the
butto n
o r selecting Windo w | AVD Manage r. The ADT dialo g windo w will o pen. Feel free to lo o k aro und in the
windo w to get an idea o f what go es o n there befo re yo u co ntinue o n to the next sectio n, where we'll create an
emulato r using the AVD Manager.
Note
Yo ur AVD Manager pro bably wo n't be empty like the screensho t abo ve. Due to the nature o f the
remo te develo pment enviro nment we're using and the way the AVD Manager handles
emulato rs, yo u'll pro bably see many o ther users' emulato rs. Co nversely, any changes yo u
make in the AVD Manager will be visible to o ther users as well. Please be respectful o f the o ther
users and do not mo dify o r delete any emulato rs o ther than tho se yo u've created fo r yo urself.
Create an Emulator
If yo u clo sed it, o pen yo ur ADT windo w again. This is the windo w that allo ws yo u to create and co nfigure as
many Andro id emulato rs as yo u like so yo u can test yo ur applicatio n o n vario us different hardware and
so ftware co nfiguratio ns. Fo r no w, we'll create a single emulato r.
On the right side o f the ADT windo w, click Ne w.... The "Create new Andro id Virtual Device (AVD)" wizard
appears.
Fo r the Name, enter your-ost-username-andro id2.2.3 (fo r example, if yo ur username is
jjam iso n, yo ur emulato r name wo uld be jjam iso n-andro id2.2.3).
In the Device dro pdo wn, select the Ne xus S.
in the Target dro pdo wn, select Andro id 2.2.3 - API Le ve l 10 .
Fo r the SD card, select the Size radio butto n and enter 20 MiB.
When yo u're ready, click Cre at e AVD at the bo tto m. Then, select yo ur new emulato r in the Virtual Devices list,
and click St art ... o n the right:
A Launch Optio ns windo w appears. The emulato r is actually a little to o big fo r o ur remo te Eclipse sessio n, so
we'll scale it do wn a little. Check the Scale display t o re al size bo x, enter 8.0 in the Screen Size (in.) field,
and then click Launch:
The emulato r will take a while to lo ad. No w might be a go o d time to po ur yo urself ano ther cup o f co ffee o r let
the do g o ut. When the emulato r is finally lo aded, yo u'll see it in ano ther windo w o n to p o f Eclipse.
At this po int, yo u can clo se the Virtual Device Manager windo w, but try no t to clo se the emulato r when
develo ping yo ur applicatio n. Yo u'll save a lo t o f time if yo u do n't have to sit thro ugh the bo o t-up pro cess o f
the emulato r. Alternatively, yo u might use the Snapshot feature in the Launch Optio ns windo w (abo ve). In
Snapsho t mo de, whenever the emulato r is clo sed, AVD saves a snapsho t o f the current state o f the emulato r,
which allo ws it to bo o t up faster. Ho wever, if yo ur emulato r ends up in a weird o r bro ken state, yo u'll need to
check the Wipe use r dat a bo x in the Launch Optio ns windo w when yo u restart it, in o rder to reset the
snapsho t state o f the emulato r.
To switch between this lesso n co ntent and the emulato r, use the tabs at the bo tto m o f the screen:
Note
Yo u can set up o ther emulato rs to match different devices, if yo u like. Always begin the emulato r
name with yo ur OST user name, so yo u can differentiate them fro m emulato rs created by o ther
users.
In the next sectio n, we'll finally dig into so me co de and run o ur first Andro id applicatio n!
Android Fragments
What are Andro id Fragments? The Andro id develo per do cumentatio n describes a fragment as "a piece o f an
applicatio n's user interface o r behavio r that can be placed in an Activity." I like to think o f fragments as an extensio n o f
Andro id's Activity pattern to better encapsulate yo ur view lo gic away fro m each specific Activity. This makes it easier to
reuse yo ur view lo gic and better suppo rt multiple screen sizes fro m pho nes to tablets. If yo u to o k the first co urse,
yo u'll remember using Fragments briefly in the Dialo gs lesso n. In this co urse, we'll use Fragments much mo re; in fact,
we'll use them in every single applicatio n we build.
Note
Do n't co nfuse Andro id Fragments with the fragmentatio n o f the Andro id platfo rm. Andro id fragmentatio n
that yo u may hear abo ut in vario us news so urces refers to the "fragmentatio n" o f the vario us different
platfo rm versio ns o f the Andro id OS installed o n each Andro id pho ne.
Let's get go ing and start a pro ject using Fragments. Create a new Andro id Pro ject. Select File | Ne w | Ot he r, and
select Andro id Applicat io n Pro je ct . Name the pro ject Fragm e nt s, enter the package name
co m .o st .andro id.f ragm e nt s, select the o ptio ns fo r the o ther values as sho wn, and click Ne xt :
Uncheck the Cre at e cust o m launche r ico n bo x, check the Add pro je ct t o wo rking se t s bo x and click Se le ct to
cho o se the Andro id2_Le sso ns wo rking set:
Click Ne xt . In the next two windo ws, keep the default cho ices:
Click Finish to create the pro ject. Next, we need to add the suppo rt library to the pro ject. ADT makes this pro cess pretty
straightfo rward. Right-click the Fragm e nt s ro o t pro ject fo lder, cho o se Andro id T o o ls | Add Suppo rt Library.
Andro id auto matically do wnlo ads the latest versio n o f the suppo rt library and includes it in yo ur pro ject. When it's
finished, verify that the pro cess wo rked by expanding the Fragm e nt s/libs fo lder to find the andro id-suppo rt -v4 .jar
file.
No w, in yo ur new pro ject, in the /src fo lder, co m .o st .andro id.f ragm e nt s package, o pen the MainAct ivit y.java file
and make these changes:
CODE TO TYPE: MainActivity.java
package com.ost.android.fragments;
import
import
import
import
android.os.Bundle;
android.app.Activity;
android.view.Menu;
android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
This co de will co mpile as is; if yo u see any red squiggles in the text, go back and make sure yo u've included the
Suppo rt Library pro perly. No w that o ur Activity suppo rts fragments, let's create a fragment. Create a new class named
Ho m e Fragm e nt , change the package name to co m .o st .andro id.f ragm e nt s and make sure it extends fro m
andro id.suppo rt .v4 .app.Fragm e nt . Yo ur New Java Class wizard lo o ks like this:
We'll co me back to this file in a bit, but first we'll ho o k this Fragment up to o ur Activity. Open the act ivit y_m ain.xm l
layo ut file in the /re s/layo ut fo lder and make these changes:
/res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/homefragment"
class="com.ost.android.fragments.HomeFragment"/>
</RelativeLinearLayout>
No w the Activity will lo ad the new fragment auto matically as so o n as the view is lo aded. Let's run it. Right-click the
Fragm e nt s ro o t pro ject name and select Run As | Andro id Applicat io n. The applicatio n will crash. Check Lo gCat
fo r the erro r (yo u may have to do uble-click o n the Lo gCat windo w tab to expand the windo w and see the erro r
message clearly):
There's a lo t o f red text here so it co uld be to ugh to find the exact info rmatio n we need. We'll lo o k fo r references to files
we've actually created in the applicatio n, which in this case is Ho meFragment. The erro r tells us "Fragment
co m.o st.andro id.fragments.Ho meFragment did no t create a view." We are o n the right track. Our Fragment is being
lo aded, but hasn't created a view yet so it's crashing the applicatio n right away. Let's fix that. Create a new Andro id XML
Layo ut file named ho m e _f ragm e nt .xm l and then mo dify it as sho wn:
CODE TO TYPE: /res/layo ut/ho me_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/home_fragment_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Home Fragment Button"/>
</LinearLayout>
No w go back to Ho meFragment.java and make these changes:
CODE TO TYPE: Ho meFragment.java
package com.ost.android.fragments;
import
import
import
import
import
android.os.Bundle;
android.support.v4.app.Fragment;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
public class HomeFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedIn
stanceState) {
return inflater.inflate(R.layout.home_fragment, container, false);
}
}
No w o ur applicatio n wo rks and sho ws the fragment lo ading successfully. Save the mo dified files and run the
applicatio n o nce mo re. Once it lo ads, yo ur emulato r lo o ks like this:
Great! So what's go ing o n here? Well, first we changed o ur usual starting Activity to extend fro m Fragm e nt Act ivit y.
We used the Andro id Suppo rt library to get access to the FragmentActivity class. If we were writing an applicatio n fo r the
Andro id 3 (Ho neyco mb) versio n o r later, we wo uldn't need the suppo rt library. As yo u might have guessed, a
FragmentActivity class is required to lo ad a Fragment.
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/homefragment"
class="com.ost.android.fragments.HomeFragment" />
</LinearLayout>
We used the MainActivity's view to lo ad the fragment. In activity_main.xml, we added the f ragm e nt xml no de to o ur
layo ut. The fragment no de tells the Activity to lo ad a Fragment and place the Fragment's view into the layo ut in its place.
The layo ut widt h and he ight pro perties are applied to the Fragment's view:
OBSERVE: Ho meFragment.java
package com.ost.android.fragments;
import
import
import
import
import
import
com.ost.android.fragments.R;
android.os.Bundle;
android.support.v4.app.Fragment;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
import com.ost.android.fragments.R;
public class HomeFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedIn
stanceState) {
return inflater.inflate(R.layout.home_fragment, container, false);
}
}
In o ur newly created Ho meFragment class (that extends the Fragm e nt class fro m the Suppo rt Library) we
implemented the o nCre at e Vie w metho d in o rder to lo ad a view fo r the Fragment pro perly. The metho d receives a
reference to a Layo ut Inf lat e r o bject, so we use that to inflate a view we defined in XML and return that inflated view.
The seco nd parameter sent to the inflate metho d is the Vie wGro up that will eventually co ntain this view. By sending
the ViewGro up co nt aine r, o ur new view will inherit the layo ut parameters fro m this ViewGro up. The last param e t e r
defines whether we want to attach this view auto matically to the co ntainer ViewGro up fro m the seco nd parameter. We
do n't want to do that tho ugh because it's already go ing to be handled auto matically in the framewo rk classes, so we
pass in f alse here.
So , Fragments are lo aded into Activities and have their o wn views. Yo u can learn mo re in the Andro id do cumentatio n
fo r the Fragment class. If yo u take a lo o k at the lifecycle, yo u see it has a similar lifecycle to that o f the Activity class.
Just like Activity, Fragment has o nCre at e , o nSt art , and o nRe sum e metho ds, as well as their co rrespo nding
deco nstructio n metho ds o nPause , o nSt o p, and o nDe st ro y. There are also so me o ther lifecycle metho ds that
distinguish Fragment fro m the Activity class.
Perhaps the mo st impo rtant difference between a Fragment and an Activity is that the Fragment class is no t an
extensio n o f Co nt e xt . Fragments get their co ntext fro m the Activity that creates them, so they canno t exist witho ut an
Activity. A Fragment can always get a reference to its parent Activity, and thus a Co ntext reference, by calling the
ge t Act ivit y() metho d. Ho wever, when implementing a Fragment yo u must make sure that the parent Activity hasn't
been destro yed. This is where the new Fragment lifecycle metho d o nAct ivit yCre at e d co mes in handy. If a Fragment
must perfo rm lo gic requiring a co ntext when it is lo aded, then yo u place that lo gic in the o nAct ivit yCre at e d metho d
where yo u can guarantee that the parent Activity has already finished its creatio n lifecycle and is ready to be used as a
Co ntext.
Using Fragments Programatically
In additio n to lo ading fragments thro ugh XML, we can lo ad them dynamically in o ur Activity. Often yo u do n't
even need a Layo ut XML fo r yo ur activity at all when lo ading Fragments pro grammatically, but we're go ing to
co ntinue using o ur previo us view here. Let's start by creating a new Fragment; name the class
Se co ndFragm e nt and o f co urse have it extend the andro id.suppo rt .v4 .app.Fragm e nt class. Also ,
make sure the file is in the co m .o st .andro id.f ragm e nt s package. No w make these changes:
CODE TO TYPE: Seco ndFragment.java
package com.ost.android.fragments;
import
import
import
import
import
android.os.Bundle;
android.support.v4.app.Fragment;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
import com.ost.android.fragments.R;
public class SecondFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
return inflater.inflate(R.layout.second_fragment, container, false);
}
}
No w let's create the XML layo ut view file fo r this fragment. As yo u might have guessed, we'll name this file
se co nd_f ragm e nt .xm l. Make sure that the file is in the /re s/layo ut / fo lder and then make these changes:
CODE TO TYPE: /res/layo ut/seco nd_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Second Fragment Loaded!" />
</LinearLayout>
No w we can clo se these new files, go back to o ur previo us co de, and update it to lo ad the new Fragment.
Open the act ivit y_m ain.xm l layo ut file and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<fragment
android:layout_height="match_parentwrap_content"
android:layout_width="match_parent"
android:id="@+id/homefragment"
class="com.ost.android.fragments.HomeFragment" />
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent" />
</LinearLayout>
Next, o pen MainAct ivit y.java and make these changes:
CODE TO TYPE: MainActivity.java
package com.ost.android.fragments;
import
import
import
import
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.support.v4.app.FragmentManager;
android.support.v4.app.FragmentTransaction;
public class MainActivity extends FragmentActivity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void loadSecondFragment() {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
SecondFragment sf = new SecondFragment();
ft.add(R.id.fragment_container, sf);
ft.commit();
}
}
Open Ho m e Fragm e nt .java and make o ne last set o f changes:
CODE TO TYPE: Ho meFragment.java
package com.ost.android.fragments;
import
import
import
import
import
android.os.Bundle;
android.support.v4.app.Fragment;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
public class HomeFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
return inflater.inflate(R.layout.home_fragment, container, false);
View view = inflater.inflate(R.layout.home_fragment, container, false);
view.findViewById(R.id.home_fragment_button).setOnClickListener(buttonClickL
istener);
return view;
}
public View.OnClickListener buttonClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity activity = (MainActivity) getActivity();
activity.loadSecondFragment();
}
};
}
Save the changed files and run the applicatio n. Yo ur emulato r lo ads and lo o ks the same as befo re, but no w
when yo u click the Ho m e Fragm e nt But t o n, it lo ads the Se co ndFragm e nt :
Let's go back o ver so me key areas no w and discuss o ur co de in detail. First, let's lo o k at the changes we
made to MainAct ivit yFragm e nt .java:
OBSERVE:
public void loadSecondFragment() {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
SecondFragment sf = new SecondFragment();
ft.add(R.id.fragment_container, sf);
ft.commit();
}
We start by getting a reference to the Fragm e nt Manage r class. This is accessed by using the
ge t Suppo rt Fragm e nt Manage r class inherited fro m FragmentActivity. Just like befo re, this is the Suppo rt
Library versio n o f the FragmentManager. If this applicatio n was targeting Ho neyco mb o r later, we'd just call
getFragmentManager to get o ur reference. This is o ne o f the few instances using Fragments where the API
name differs in the Suppo rt Library fro m the latest SDK.
We initiate a Fragm e nt T ransact io n that will define the Fragment changes that are abo ut to o ccur. Every
time a change is made to an Activity's Fragments, a Fragm e nt T ransact io n must be used.
Then we create an instance o f o ur Se co ndFragm e nt , and update the Fragm e nt T ransact io n, telling it to
add o ur fragment to the ViewGro up in this Activity's view with the co rrespo nding id
R.id.f ragm e nt _co nt aine r. Finally, we call co m m it o n the transactio n to finalize o ur changes.
OBSERVE: Ho meFragment.java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View view = inflater.inflate(R.layout.home_fragment, container, false);
view.findViewById(R.id.home_fragment_button).setOnClickListener(buttonClickL
istener);
return view;
}
public View.OnClickListener buttonClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity activity = (MainActivity) getActivity();
activity.loadSecondFragment();
}
};
We also made so me interesting changes to o ur Ho m e Fragm e nt .java. First, we mo dified the
o nCre at e Vie w metho d in o rder to set a click listener o n o ur Butto n. Yo u might be used to implementing click
handlers fo r Butto ns in XML Layo uts using the o nClick attribute co nventio n. Unfo rtunately, a Fragment
canno t use that co nventio n, so click listeners must be set using the se t OnClickList e ne r metho d o n the
Butto n directly. If an o nClick attribute metho d is defined in the View, Andro id will still attempt to call a metho d
with that name o n the o wning Activity (that is, the activity class that's created when yo u create the pro ject,
MainActivity.java), even if the View was defined in a Fragment; ho wever, we are writing o ur co de so that o ur
Activities do n't need to manage the co ntents o f the Fragment's views, so we keep this lo gic co ntained in the
Fragment itself.
In o ur click listener, we used the ge t Act ivit y metho d (inherited fro m the Fragment class) to get a reference to
o ur FragmentActivity. We kno w that this Fragment will belo ng to a MainAct ivit y class, so we can safely cast
o ur reference to that class. Finally, we call the lo adSe co ndFragm e nt metho d o n o ur activity to start the
lo ading pro cess.
Wrapping Up
We've co vered the basics o f Fragments in Andro id in this lesso n, but there's still mo re functio nality to explo re. Check
the Andro id Develo per Do cumentatio n Site fo r mo re detailed info rmatio n regarding the entire Fragment pro cess. We'll
be using Fragments, o r at the very least FragmentActivity, in every lesso n fo r this co urse, so make sure yo u feel
co mfo rtable with the basics we've learned here befo re yo u go o n.
Practice what yo u've learned in the ho mewo rk. See yo u in the next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Loaders
Lesson Objectives
In this lesso n, yo u will:
write and implement a Lo ader.
replace an AsyncTask with a Lo ader.
implement Lo aderCallbacks to handle Lo ader results.
register a Lo ader and Lo aderCallbacks with the Lo aderManager.
Welco me back! In this lesso n we'll co ver Lo aders, a great new feature in Andro id that helps to lo ad data asynchro no usly.
Lo aders are managed o utside o f the sco pe o f an activity, which allo ws us to retrieve data fro m a Lo ader even if the activity has
been destro yed and recreated (like when the user ro tates the screen). Like Fragments, Lo aders first became available in API 11
(Ho neyco mb), and are available to applicatio ns targeting earlier APIs thro ugh the suppo rt library.
Why Use a Loader?
At first glance, Lo aders might no t seem vital. After all, we already have AsyncTasks to perfo rm lo ng-running
pro cesses. Ho wever, AsyncTasks do n't exactly co o perate with Andro id's life-cycle fo r Views and Fragments. The
example will help illustrate the need fo r Lo aders.
Let's get started. Create a new Andro id pro ject using these criteria:
Name the pro ject Lo ade rs.
Use the package name co m .o st .andro id.lo ade rs.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
We'll begin by demo nstrating the sho rtco mings o f AsyncTask. In MainAct ivit y.java, make these changes:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.loaders;
import
import
import
import
import
import
android.app.Activity;
android.os.AsyncTask;
android.os.Bundle;
android.text.format.DateUtils;
android.widget.TextView;
android.view.Menu;
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AsyncTask<Void, Void, String> myTask = new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS * 5);
} catch (InterruptedException e) {
}
return "AsyncTask Complete!";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
TextView tv = (TextView) findViewById(R.id.text);
tv.setText(result);
}
};
myTask.execute();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
We also need to make so me mino r edits to act ivit y_m ain.xm l:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" />
</RelativeLayout>
Save yo ur changes and run the pro ject. Yo u see this:
Then, after abo ut five seco nds (depending o n ho w fast the emulato r is running thro ugh the Virtual Deskto p), the screen
updates:
OBSERVE: MainActivity.java
.
.
.
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AsyncTask<Void, Void, String> myTask = new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS * 5);
} catch (InterruptedException e) {
}
return "AsyncTask Complete!";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
TextView tv = (TextView) findViewById(R.id.text);
tv.setText(result);
}
};
myTask.execute();
}
}
This little applicatio n demo nstrates running an AsyncT ask pro cess that takes abo ut five seco nds to finish. When the
task is started, it calls T hre ad.sle e p() in its do InBackgro und() metho d. This causes the executio n in that Thread to
pause o n this line fo r five seco nds. After waiting five seco nds, the thread co ntinues and finishes the
do InBackgro und() metho d, returning the St ring value. In the o nPo st Exe cut e () metho d, the resulting St ring value
is finally applied to the T e xt Vie w.
Note
Yo u sho uld never actually use T hre ad.sle e p() in yo ur Andro id applicatio ns. We're just using it here to
simulate so mething that takes five seco nds to co mplete. If yo u need to schedule so mething to o ccur after
a sho rt perio d o f time in yo ur Applicatio ns, co nsider using the Timer and TimerTask classes instead. Yo u
might also co nsider using a Se rvice , which we'll co ver in a bit.
No w, ro tate the emulato r. Fo cus o n the emulato r windo w and press [Ct rl+F12] o n yo ur keybo ard. The emulato r
ro tates to landscape mo de. Also , the TextView in the middle o f the screen go es back to displaying the previo us
message, "Hello Wo rld, MainActivity!" Then, after ano ther five seco nds, the message "AsyncTask Co mplete!" displays
o nce mo re. This will happen each time yo u ro tate the emulato r. Go ahead and try it a co uple o f times.
Note
[Ct rl+F11] and [Ct rl+F12] will bo th ro tate the emulato r. To find o ut abo ut mo re emulato r keybo ard
sho rtcuts, see the do cumentatio n site.
So , let's see what's really go ing o n here. Every time an Andro id device ro tates, the Andro id system destro ys the
current Activity (and any active Fragments) and then recreates them in the new o rientatio n. Applicatio ns can explicitly
prevent this fro m happening, but at the co st o f its ability to use a separate layo ut per o rientatio n auto matically. So me
applicatio ns will prevent this by disabling ro tatio n, thereby fo rcing the user to use the applicatio n in their specified
o rientatio n. Generally, neither o f these strategies are reco mmended. It is better to learn ho w to use the Applicatio n lifecycle to preserve the state o f the applicatio n during a ro tatio n and resto re the state when the Activity/Fragment is
resto red.
Here, we are requesting o ur data fro m a pro cess that takes so me time to return. We do n't want to re-request the data
each time the device ro tates. We co uld pass the necessary data to the next Activity using the Andro id life-cycle
metho ds (specifically o nSave Inst ance St at e ()), but the user co uld also ro tate befo re the task actually finishes. We
sho uldn't have to start o ur request o ver just because the user ro tated befo re the task returned. This is where Lo aders
co me in handy.
Performing T asks in a Loader
Let's update the Applicatio n to use a Lo ader instead o f an AsyncTask. Edit MainAct ivit y.java as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.loaders;
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.os.AsyncTask;
android.content.Context;
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.support.v4.app.LoaderManager;
android.support.v4.content.AsyncTaskLoader;
android.support.v4.content.Loader;
android.text.format.DateUtils;
android.widget.TextView;
public class MainActivity extends Activity {
public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallb
acks<String> {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AsyncTask<Void, Void, String> myTask = new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS * 5);
} catch (InterruptedException e) {
}
return "AsyncTask Complete!";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
TextView tv = (TextView) findViewById(R.id.text);
tv.setText(result);
}
};
myTask.execute();
LoaderManager lm = getSupportLoaderManager();
Loader<String> loader = lm.initLoader(0, null, this);
if (!loader.isStarted())
loader.forceLoad();
}
@Override
public Loader<String> onCreateLoader(int loaderId, Bundle args) {
return new MyLoader(this);
}
@Override
public void onLoadFinished(Loader<String> loader, String data) {
TextView tv = (TextView) findViewById(R.id.text);
tv.setText(data);
}
@Override
public void onLoaderReset(Loader<String> loader) {
}
private static class MyLoader extends AsyncTaskLoader<String> {
public MyLoader(Context context) {
super(context);
}
@Override
public String loadInBackground() {
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS * 5);
} catch (InterruptedException e) {
}
return "AsyncTaskLoader Complete!";
}
}
}
Make sure yo u're impo rting the Suppo rt Library versio n o f the FragmentActivity, Lo aderManager, AsyncTaskLo ader,
and Lo ader classes. No w, save yo ur changes and run the Applicatio n again. Test the ro tatio n. Test ro tating befo re the
first five seco nds are even up. No tice anything different? The Applicatio n no w takes o nly five seco nds to tal to switch the
TextView to say "AsyncTaskLo ader Co mplete!" Even if the five seco nds runs o ut during a ro tatio n, the View reflects the
result immediately when recreated.
Alright, so this is pretty co o l, but what's happening? Why is this different fro m the AsyncTask? Let's walk thro ugh this
co de step by step, starting with o ur additio ns to the o nCre at e () metho d.
OBSERVE: MainActivity.java - o nCreate()
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LoaderManager lm = getSupportLoaderManager();
Loader<String> loader = lm.initLoader(0, null, this);
.
.
.
We start by getting an instance o f the Lo ade rManage r fro m the Activity by calling ge t Suppo rt Lo ade rManage r().
Then we get an instance o f the Lo ade r we want, using the init Lo ade r() metho d o n the Lo ade rManage r.
init Lo ade r() takes three parameters. The f irst , an Integer, is an ID that can be used to help the callbacks identify
which type o f Lo ader it sho uld create. We have o nly o ne type o f Lo ader in this ro utine, so this value do esn't really
matter. The ne xt param e t e r is a Bundle o bject that can be used to send so me additio nal data to the ro utine that
creates the Lo ader, such as data that might be needed in the co nstructo r o f the Lo ader. We do n't have any data like
this, so we simply send null. The last param e t e r is the mo st impo rtant fo r o ur co de. It requires an instance o f the
Lo ade rManage r.Lo ade rCallbacks<T > interface. The interface has a generic defined, which must match the Generic
used in the definitio n o f the Lo ader class.
OBSERVE:
.
.
.
if (!loader.isStarted())
loader.forceLoad();
Next, we che ck t o se e whe t he r o r no t t he Lo ade r has act ually be e n st art e d ye t , and if no t, f o rce it t o st art
right away. There's ano ther metho d o n the Lo ader class called st art Lo ade r() which might seem like the metho d to
call when yo u want to start the Lo ader, but in this case it isn't. Our Lo ader is an implementatio n o f the
AsyncT askLo ade r class, which requires yo u to call the f o rce Lo ad() metho d o n the Lo ade r to kick o ff its request
pro cess. Next, we'll lo o k at the changes to the class definitio n.
OBSERVE: MainActivity class
public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallb
acks<String>
Our regular "pre-Ho neyco mb" Activity class do esn't actually suppo rt getting an instance o f the Lo ade rManage r class,
so we need to change o ur class to extend the suppo rt library versio n o f Fragm e nt Act ivit y instead. Also , in o rder to
send "this" to the Lo ade rManage r.init Lo ade r() metho d, we have to im ple m e nt t he Lo ade rCallbacks interface
as well. We want o ur Lo ade r to return a St ring value, so we define the generic parameter here as the St ring class.
Next, we'll lo o k at the metho ds required to implement Lo ade rCallbacks:
OBSERVE: MainActivity.java - Lo aderCallbacks metho ds
@Override
public Loader<String> onCreateLoader(int loaderId, Bundle args) {
return new MyLoader(this);
}
@Override
public void onLoadFinished(Loader<String> loader, String data) {
TextView tv = (TextView) findViewById(R.id.text);
tv.setText(data);
}
@Override
public void onLoaderReset(Loader<String> data) {
}
Lo aderManager.Lo aderCallbacks<T> requires three metho ds. The first metho d, o nCre at e Lo ade r(), has two
parameters. The f irst param e t e r, an Int e ge r, is the same Integer that was passed to the Lo aderManager.initLo ader
earlier. We do n't need to use this parameter in o ur implementatio n. The se co nd param e t e r is a Bundle o bje ct and,
o f co urse, co rrespo nds to the seco nd parameter sent to Lo ade rManage r.init Lo ade r earlier. Again, we're no t using
this parameter, so we just igno re it. The mo st impo rtant requirement o f this metho d is that it returns a Lo ade r o bject
that implements the Generic parameter defined fo r this implementatio n. We just create a new instance o f o ur MyLo ader
class. All Lo ade rs require a Co nt e xt in the co nstructo r, and since o ur Fragm e nt Act ivit y is a Co ntext, we just pass
this to the MyLo ader co nstructo r. This metho d is actually called internally by the Lo ade rManage r class the first time
init Lo ad() is called o n the Lo ade rManage r. Lo ade rManage r will then cache the Lo ade r and return the cache value
o n subsequent calls to init Lo ade r().
The seco nd metho d, o nLo adFinishe d(), is called after the Lo ade r finishes lo ading its data. The callback metho d
receives two parameters. T he f irst is a re f e re nce t o t he Lo ade r that perfo rmed the wo rk. T he se co nd
param e t e r is t he dat a re sult ; in o ur case, it will co ntain the St ring value "AsyncTaskLo ader Co mplete!" This
callback metho d will always be called o n the UI thread, so it is perfectly safe to mo dify UI co mpo nents here. We apply
the text data to o ur T e xt Vie w here so we can see the results o n the screen.
The final callback metho d is o nLo ade rRe se t (). It takes just o ne parameter, the Lo ader that was created in
init Lo ade r(). This metho d is called auto matically by the Lo ade rManage r when the Lo ade r's data is abo ut to be
released and will no lo nger be available. This gives yo u the o ppo rtunity to update yo ur view to respo nd acco rdingly.
Fo r example, if the lo ader is being reset and given new parameters, this metho d wo uld be called befo re the new lo ad,
allo wing yo u to update yo ur view and invalidate the o ld data.
Next we'll take a clo se lo o k at the Lo ader we created.
OBSERVE: MyLo ader class
private static class MyLoader extends AsyncTaskLoader<String> {
public MyLoader(Context context) {
super(context);
}
@Override
public String loadInBackground() {
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS * 5);
} catch (InterruptedException e) {
}
return "AsyncTaskLoader Complete!";
}
}
When yo u create a custo m lo ader to perfo rm backgro und data, it's usually better (and easier) to e xt e nd t he
AsyncT askLo ade r class than the base Lo ader class. AsyncTaskLo ader, as the name implies, actually uses an
AsyncTask o bject internally so yo u do n't have to wo rry abo ut managing any o f the threading lo gic. With
AsyncTaskLo ader, yo u o nly need to implement o ne metho d: lo adInBackgro und(). This is where yo u perfo rm the
wo rk required to lo ad the data. Thanks to the underlying AsyncT ask, lo adInBackgro und() is already called o n a
separate thread. As yo u can see, we just co pied o ur co de fro m the AsyncT ask into the metho d here.
Note
Time perio ds in Andro id/Java are o ften expressed in milliseco nds. The Dat e Ut ils class has so me
excellent features to help manage time units. We used Dat e Ut ils in this lesso n to explicitly define a
perio d o f 5 seco nds. Yo u co uld just as easily hard-co de 50 0 0 here (50 0 0 milliseco nds = 5 seco nds
after all), but using Dat e Ut ils co nstants makes it easier to read so yo u (o r anyo ne else reading yo ur
co de) will kno w immediately what interval is intended. There are also o ther co nstants such as
HOUR_IN_MILLIS and DAY_IN_MILLIS which help with quantities where the math is co nsiderably mo re
difficult to read in an instant.
So , let's recap the flo w here step-by-step. In the o nCre at e () metho d we ask the Lo ade rManage r fo r an instance o f
o ur Lo ade r. Lo ade rManage r then calls o nCre at e Lo ade r() o n the Lo ade rCallbacks implementatio n where we
create o ur instance o f o ur MyLo ade r class. Lo ade rManage r caches this, and returns the reference in init Lo ade r().
Then we check to determine whether the Lo ade r is already started, and if it isn't we fo rce it to start by calling
f o rce Lo ad(). This causes o ur MyLo ade r instance to create a new thread and start its wo rk in the
lo adInBackgro und() metho d. When the wo rk is co mplete, the Lo ader returns its data, which is cached in the
Lo ade rManage r. Lo ade rManage r then calls o nLo ade rFinishe d() o n the Lo ade rCallbacks, where we present
the data result.
Next, whenever the device is ro tated, o ur Fragm e nt Act ivit y gets destro yed and created. o nCre at e () gets called
o nce mo re, where we o nce again ask the Lo ade rManage r fo r an instance o f o ur Lo ade r. It already has a reference
cached, so it returns the reference immediately. Then we check to determine whether the Lo ader is started already; it is,
so we do no thing. Lo ade rManage r will also check to find o ut if the Lo ade r instance has co mpleted o r no t, and if it
has, it will immediately call o nLo ade rFinishe d() o n the Lo ade rCallbacks, passing the cached data.
Wrapping Up
As yo u can see, Lo aders can help co nsiderably when yo u need to perfo rm lo ng-running tasks. In fact, yo u handle
mo st backgro und tasks in yo ur applicatio ns with either a Lo ade r o r a Se rvice . There is o ne unfo rtunate do wnside to
Lo aders tho ugh—a lack o f suppo rt fo r repo rting pro gress. AsyncT ask made that task relatively easy, but repo rting
pro gress with a Lo ader takes co nsiderably mo re effo rt and co de to implement cleanly. One alternate so lutio n is to
sho w an indeterminate pro gress bar when yo u start a lo ad. If yo ur backgro und lo ads are lo ng eno ugh that yo u need to
present accurate pro gress to the user, co nsider using a Se rvice and Binde r messages to repo rt pro gress to yo ur
Views. We will co ver Se rvice s and Binde rs later in the co urse.
The last type o f Lo ade r class left fo r us to explo re is the Curso rLo ade r. It's a subclass o f AsyncT askLo ade r. This
type o f Lo ader is used to manage Curso r o bjects that encapsulate data results fro m a Co nt e nt Pro vide r.
Co nt e nt Pro vide rs and Curso rs will also be co vered so o n, in the upco ming Co ntent Pro viders lesso n.
Practice what yo u've learned in this lesso n in yo ur ho mewo rk. See yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Advanced Layouts
Lesson Objectives
In this lesso n yo u will:
create alternate layo uts using reso urce qualifiers.
reuse a fragment in bo th tablet and pho ne layo uts.
use a single activity to handle bo th pho ne and tablet layo uts using fragments.
Welco me back! In previo us lesso ns we've created layo uts fo r o ur applicatio ns using XML, but we've still o nly scratched the
surface. In this lesso n we'll go o ver so me o f the mo re advanced to o ls and features available fo r making layo uts in Andro id,
including alternate layo uts fo r o rientatio n and screen size (that is, fo r pho nes and tablets) as well as so me layo ut o ptimizatio n
to o ls.
Let's get started. Create a new Andro id pro ject using these criteria:
Name the pro ject Advance dLayo ut s.
Use the package name co m .o st .andro id.advance dlayo ut s.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
We'll need a tablet-sized emulato r fo r this lesso n. Click the Andro id Virt ual De vice Manage r butto n (
) at the to p o f the
Eclipse windo w. In the Device Manager windo w, click Ne w. In the Create New Andro id Virtual Device (AVD) windo w, give the
device an appro priate name, like username-t ab-WXGA-4 .3-18. Fo r Device, select 10 .1" WXGA (T able t ) (1280 x 80 0 :
m dpi). Set the Target to Andro id 2.3.3 - API Le ve l 10 . Fo r CPU/ABI, select the ARM (arm e abi-v7 a) o ptio n. Fo r SD card,
select a Size o f 20 MiB. Click OK. Back in the Andro id Virtual Device Manager windo w, make sure the new AVD is selected and
click St art ... to start the emulato r. In the Launch Optio ns dialo g, select Scale display t o re al size , enter 8 fo r the screen size,
and click Launch.
Supporting Orientation Changes
The way Andro id handles ro tatio n can be a bit pro blematic at times. The Andro id OS will co mpletely destro y and
recreate the fro nt Activity (and its Fragments) during ro tatio n. If the applicatio n develo per isn't prepared to handle this, it
can lead to a very frustrating user experience, and po ssibly even crash the who le applicatio n. Fo rtunately, the Andro id
life-cycle pro vides so me ho o ks that allo w us to persist the state o f o ur Activity thro ugh a ro tatio n. This pro cess also
makes it co nvenient to change the layo ut o f o ur View depending o n the o rientatio n o f the device. Let's practice do ing
that. First, we'll update o ur primary View to make it a little mo re interesting. Open the layo ut file act ivit y_m ain.xm l
and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<EditText
android:id="@+id/inputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp" />
<Button
android:id="@+id/reverseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/inputEditText"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="Reverse" />
<TextView
android:id="@+id/reverseTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/reverseButton"
android:layout_margin="20dp" />
</RelativeLayout>
No w, update MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.ost.android.advancedlayouts;
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.EditText;
android.widget.TextView;
android.view.Menu;
public class MainActivity extends FragmentActivity {
EditText inputEditText;
TextView reverseTextView;
Button reverseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputEditText = (EditText) findViewById(R.id.inputEditText);
reverseTextView = (TextView) findViewById(R.id.reverseTextView);
reverseButton = (Button) findViewById(R.id.reverseButton);
reverseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String inputText = inputEditText.getText().toString();
String reversedText = new StringBuffer(inputText).reverse().toString();
reverseTextView.setText(reversedText);
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Run the applicatio n, type so mething into the EditText text area, then click the Re ve rse butto n. Yo u see yo ur text
reversed in the TextView area.
No w that we have a basic functio ning applicatio n, let's implement a unique layo ut fo r landscape o rientatio n. Click File |
Ne w | Ot he r, cho o se Andro id XML Layo ut File fro m the list, and click Ne xt . Select the Advance dLayo ut s pro ject,
name the file act ivit y_m ain.xm l (yes, the same name as o ur existing layo ut file), select Line arLayo ut as the ro o t
element, and click Ne xt :
On this screen yo u'll select the reso urce qualifiers fo r this view. Select Orie nt at io n fro m the list, click the right arro w in
the middle o f the windo w and, in the "Screen Orientatio n" dro p-do wn, select Landscape . Fo r the Fo lder field, enter
/re s/layo ut -land:
Click Finish. ADT creates a new act ivit y_m ain.xm l file in a new fo lder named layo ut -land. Mo dify this new layo ut
file as sho wn:
CODE TO TYPE: /res/layo ut-land/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:id="@+id/inputEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/reverseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reverse" />
</LinearLayout>
<TextView
android:id="@+id/reverseTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp" />
</LinearLayout>
Befo re go ing any further, take a mo ment to co mpare this file and the o ther activity_main.xml file fro m the /re s/layo ut
fo lder. What's different? What stayed the same? No tice that the android:id attributes we've used fo r each o f o ur views
are exactly the same. We'll explain this so o n, but first, test the applicatio n.
Ro tate the emulato r (by pressing [Ct rl+F12]) and no tice the changes in the view layo ut:
So what happened here, and what's with the layo ut -land fo lder?
OBSERVE: MainActivity.java
...
public class MainActivity extends FragmentActivity {
EditText inputEditText;
TextView reverseTextView;
Button reverseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputEditText = (EditText) findViewById(R.id.inputEditText);
reverseTextView = (TextView) findViewById(R.id.reverseTextView);
reverseButton = (Button) findViewById(R.id.reverseButton);
reverseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String inputText = inputEditText.getText().toString();
String reversedText = new StringBuffer(inputText).reverse().toString();
reverseTextView.setText(reversedText);
}
});
}
}
Just like multiple drawable fo lders with different reso lutio n qualifiers (which we co vered in the first co urse), the layo ut land fo lder has a qualifier, but instead o f that qualifier defining an alternate device resolution, it defines a device
orientation, in this case the landscape o rientatio n. When we set the View reso urce id fo r o ur Activity, we pass the value
R.layo ut .act ivit y_m ain. Andro id tries to lo ad the mo st specific type o f file matching this id first; if it do esn't find a
layo ut reso urce in any fo lder with qualifiers that match the device's current co nfiguratio n, it falls back o n the default file
in the fo lder with no qualifiers.
Yo u co uld also create a new act ivit y_m ain.xm l layo ut file in a /re s/layo ut -po rt fo lder. Layo ut files in this fo lder
wo uld then be used fo r po rtrait o rientatio n. If yo u did this, yo u technically wo uldn't need a act ivit y_m ain.xm l in the
default /re s/layo ut fo lder. Ho wever, we do n't reco mmend remo ving that default activity_main.xml. Always keep a
default layo ut file fo r each view and o nly put specialized layo ut files fo r different device co nfiguratio ns in their respective
reso urce qualifier fo lders as needed. If an applicatio n attempts to lo ad a layo ut file and it can't find a file in any fo lder
with matching qualifiers, the applicatio n will crash.
Persisting Data on Rotation
Yo u might have no ticed a pro blem in the previo us applicatio n while ro tating the emulato r. While the text yo u typed in
the EditText is still present after a ro tatio n, the TextView no lo nger co ntains the reversed text. The EditText co mpo nent
has an "auto -save" feature in Andro id that allo ws it to auto matically persist its data o n ro tatio n, but the TextView
co mpo nent do es no t have this feature. Many applicatio ns never have to wo rry abo ut this limitatio n o f the TextView
co mpo nent. Often the TextView text will already be defined by a String reso urce co nstant, and thus will be lo aded each
time the layo ut is lo aded, even o n ro tatio n. Or perhaps the o nCre at e Vie w() metho d is auto matically defining the text
fo r each TextView. But, o bvio usly, there are so me situatio ns (like o urs) where the data sho uld pro bably be persisted
thro ugh ro tatio n.
We co uld just "reco mpute" the reversed String after a ro tatio n, but there's a better and mo re reliable way to make the
fix. Mo dify MainAct ivit y.java o nce again as sho wn:
CODE TO TYPE: MainActivity.java
package com.ost.android.advancedlayouts;
import
import
import
import
import
import
import
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.EditText;
android.widget.TextView;
public class MainActivity extends FragmentActivity {
public static final String KEY_REVERSED_TEXT = "keyReversedText";
EditText inputEditText;
TextView reverseTextView;
Button reverseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputEditText = (EditText) findViewById(R.id.inputEditText);
reverseTextView = (TextView) findViewById(R.id.reverseTextView);
reverseButton = (Button) findViewById(R.id.reverseButton);
reverseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String inputText = inputEditText.getText().toString();
String reversedText = new StringBuffer(inputText).reverse().toString();
reverseTextView.setText(reversedText);
}
});
if (savedInstanceState != null) {
String reversedText = savedInstanceState.getString(KEY_REVERSED_TEXT);
reverseTextView.setText(reversedText);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_REVERSED_TEXT, reverseTextView.getText().toString());
}
}
Run the applicatio n and test the changes. The reversed text remains o n the screen after ro tatio n.
Let's walk thro ugh these changes to make sure yo u understand them. First, lo o k at the
o nSave Inst ance St at e (Bundle o ut St at e ) metho d:
OBSERVE: MainActivity.java - o nSaveInstanceState
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_REVERSED_TEXT, reverseTextView.getText().toString());
}
Here we o verride the o nSave Inst ance St at e (Bundle o ut St at e ) metho d, a metho d o n Activity (also o n Fragments)
that is called when the Activity is go ing away fo r any reaso n and the state o f the Activity needs to be saved. The metho d
receives a Bundle o bject, which is intended to be used to sto re the current state o f the Activity. A Bundle is a unique
Andro id class that has many metho ds fo r sto ring data o f vario us types. It also has matching metho ds fo r reading
previo usly sto red data o f each type. The data types suppo rted are mo stly just the standard primitive types, like int and
lo ng; Bundles can also sto re String, Array, ArrayList, Parcelable, and Serializable o bject types, which typically are used
fo r mo re co mplex state mo dels.
We o nly need to sto re a simple String o n Bundle. The values are sto red in a Key-Value pattern similar to a HashMap.
Ho wever, unlike a HashMap, the Key in a Bundle must be a String. We use a st at ic f inal defined String key co nstant
KEY_REVERSED_T EXT to make sure we're using the same key to sto re and retrieve the data.
OBSERVE: MainActivity.java - o nCreate(Bundle savedInstanceState)
if (savedInstanceState != null) {
String reversedText = savedInstanceState.getString(KEY_REVERSED_TEXT);
reverseTextView.setText(reversedText);
}
We've seen the parameter "Bundle savedInstanceState" that's passed to the o nCreate metho d many times, but never
used it. As yo u might have guessed, this save dInst ance St at e o bject will have all the state data yo u saved
previo usly to the o ut St at e o bject in the o nSave Inst ance St at e metho d. We must do a null-check first, because this
o bject will always be null the first time the Activity is created. After that, we retrieve o ur saved state data using the same
key we used to save the data—in this case, o ur re ve rse d St ring. Take no te that yo u are no t limited to just o ne
variable at a time, so feel free to sto re all the state data yo u need between ro tatio ns o n the o ut St at e o bject.
Supporting Multiple Screen Sizes
No w that yo u've seen ho w to suppo rt alternate layo uts, we'll go o ver ho w to suppo rt alternate screen sizes. The
pro cess is similar to that used to create alternate layo uts fo r po rtrait and/o r landscape. Ho wever, keep in mind that
alternate layo uts fo r screen sizes can make wo rking with yo ur yo ur Activities and Fragments mo re difficult. We'll begin
by making a layo ut fo r a tablet device. We'll use the Andro id XML Layo ut wizard again to create an alternate
activity_main.xml file.
Click File | Ne w | Ot he r and then cho o se Andro id XML Layo ut File fro m the list. Select the Advance dLayo ut s
pro ject. Name the file act ivit y_m ain.xm l, and click Ne xt . No w, fro m the list o n the left side, cho o se Sm alle st
Scre e n Widt h and click the right-po inting arro w. In the Smallest Screen Width field that appears o n the right, type 6 0 0 .
In the Fo lder field o n the bo tto m o f the wizard, yo u can see a preview o f the fo lder qualifier name that will be generated
fo r this new layo ut file: /res/layo ut-sw6 0 0 dp. Click Finish.
In the newly created /re s/layo ut -sw6 0 0 dp/act ivit y_m ain.xm l layo ut file, add this co de:
CODE TO TYPE: /res/layo ut-sw6 0 0 dp/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" >
<EditText
android:id="@+id/inputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp" />
<Button
android:id="@+id/reverseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/inputEditText"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="Reverse" />
</RelativeLayout>
<fragment
android:id="@+id/fragResult"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
class="com.ost.android.advancedlayouts.ResultFragment" />
</LinearLayout>
No w, make a new layo ut fo r the new fragment. Select File | Ne w | Ot he r, and in the po pup menu, cho o se Andro id
XML Layo ut File . Name the file f ragm e nt _re sult , select Re lat ive Layo ut , and click Finish. Add the same result
TextView fro m the o riginal layo ut into f ragm e nt _re sult .xm l as sho wn:
CODE TO TYPE: /res/layo ut/fragment_result.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/reverseTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_centerInParent="true" />
</RelativeLayout>
Next, create the new Fragment class. Right-click the co m .o st .andro id.advance dlayo ut s package and select Ne w |
Class. Name the class Re sult Fragm e nt , set the Superclass to andro id.suppo rt .v4 .app.Fragm e nt , and click
Finish. Mo dify the new class as sho wn:
CODE TO TYPE: ResultFragment.java
package com.ost.android.advancedlayouts;
import
import
import
import
import
import
android.os.Bundle;
android.support.v4.app.Fragment;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
android.widget.TextView;
public class ResultFragment extends Fragment {
private TextView reverseTextView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedIn
stanceState) {
View view = inflater.inflate(R.layout.fragment_result, container, false);
reverseTextView = (TextView) view.findViewById(R.id.reverseTextView);
return view;
}
public void setReverseText(String reverseText) {
reverseTextView.setText(reverseText);
}
}
Create ano ther Andro id XML Layo ut file named re sult _act ivit y.xm l, selecting Fram e Layo ut as the ro o t element:
CODE TO TYPE: result_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="@+id/fragResult"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.ost.android.advancedlayouts.ResultFragment" />
</FrameLayout>
Yo u also need to make a new Activity that will be used to lo ad the ResultFragment o n smaller screens. Right-click the
co m .o st .andro id.advance dlayo ut s package and select Ne w | Class. Name the class Re sult Act ivit y, set the
Superclass to andro id.suppo rt .v4 .app.Fragm e nt Act ivit y, and click Finish. Mo dify the new class as sho wn:
CODE TO TYPE: ResultActivity.java
package com.ost.android.advancedlayouts;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class ResultActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.result_activity);
Bundle extras = getIntent().getExtras();
String reversedText = extras.getString(MainActivity.KEY_REVERSED_TEXT);
ResultFragment f = (ResultFragment) getSupportFragmentManager().findFragmentById(R.
id.fragResult);
if (f != null) {
f.setReverseText(reversedText);
}
}
}
In o rder to use the new Activity that yo u created, yo u need to update the Andro idManif e st . Open it and mo dify the
co de as sho wn:
CODE TO TYPE: /Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ost.android.advancedlayouts"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.ost.android.advancedlayouts.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.ost.android.advancedlayouts.ResultActivity"
android:label="Result Activity" />
</application>
</manifest>
Make so me changes to the o riginal Activity and views to utilize these new classes. Mo dify the MainAct ivit y class as
sho wn:
CODE TO TYPE: MainActivity.java
package com.ost.android.advancedlayouts;
import
import
import
import
import
import
import
import
import
android.content.Intent;
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.support.v4.app.FragmentManager;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.EditText;
android.widget.TextView;
public class MainActivity extends FragmentActivity {
public static final String KEY_REVERSED_TEXT = "keyReversedText";
EditText inputEditText;
TextView reverseTextView;
Button reverseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputEditText = (EditText) findViewById(R.id.inputEditText);
reverseTextView = (TextView) findViewById(R.id.reverseTextView);
reverseButton = (Button) findViewById(R.id.reverseButton);
reverseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String inputText = inputEditText.getText().toString();
String reversedText = new StringBuffer(inputText).reverse().toString();
reverseTextView.setText(reversedText);
FragmentManager fm = getSupportFragmentManager();
ResultFragment frag = (ResultFragment) fm.findFragmentById(R.id.fragResult);
if (frag != null) {
frag.setReverseText(reversedText);
} else {
final Intent i = new Intent(MainActivity.this, ResultActivity.class);
Bundle extras = new Bundle();
extras.putString(KEY_REVERSED_TEXT, reversedText);
i.putExtras(extras);
startActivity(i);
}
}
});
if (savedInstanceState != null) {
String reversedText = savedInstanceState.getString(KEY_REVERSED_TEXT);
reverseTextView.setText(reversedText);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_REVERSED_TEXT, reverseTextView.getText().toString());
}
}
No w, remo ve the result T e xt Vie w fro m the o riginal layo ut files, because that's go ing to be handled by o ur fragment
no w. Mo dify /re s/layo ut /act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<EditText
android:id="@+id/inputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp" />
<Button
android:id="@+id/reverseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/inputEditText"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="Reverse" />
<TextView
android:id="@+id/reverseTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/reverseButton"
android:layout_margin="20dp" />
</RelativeLayout>
Remo ve that TextView fro m the landscape layo ut. Mo dify /re s/layo ut -land/act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: /res/layo ut-land/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:id="@+id/inputEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/reverseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reverse" />
</LinearLayout>
<TextView
android:id="@+id/reverseTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp" />
</LinearLayout>
With that last change, we sho uld be go o d to go ! Run the applicatio n and test it. Co mpare the standard emulato r to the
larger tablet-sized emulato r to see ho w the app behaves.
On a pho ne-sized emulato r, after typing so me text into the first edit field and clicking the Reverse butto n, the applicatio n
launches a seco nd activity sho wing just the reversed versio n o f the o riginal text.
-->
On a tablet-sized emulato r, instead o f a new Activity, yo u see the reversed text immediately o n the right side o f the
screen.
Until no w, we've been using Fragments in almo st exactly the same way as Activities. Here, finally, we've demo nstrated
o ne o f the primary reaso ns Fragments were intro duced into the Andro id SDK. Alternate layo uts based o n device
screen size are perhaps the best use case fo r Fragments. We've made a number o f changes; let's lo o k at them in
detail no w:
OBSERVE: MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
inputEditText = (EditText) findViewById(R.id.inputEditText);
reverseButton = (Button) findViewById(R.id.reverseButton);
reverseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String inputText = inputEditText.getText().toString();
String reversedText = new StringBuffer(inputText).reverse().toString();
FragmentManager fm = getSupportFragmentManager();
ResultFragment frag = (ResultFragment) fm.findFragmentById(R.id.fragResult);
if (frag != null) {
frag.setReverseText(reversedText);
} else {
final Intent i = new Intent(MainActivity.this, ResultActivity.class);
Bundle extras = new Bundle();
extras.putString(KEY_REVERSED_TEXT, reversedText);
i.putExtras(extras);
startActivity(i);
}
}
});
}
Here in MainAct ivit y.java, we update the OnClickList e ne r with so me co nditio nal lo gic. We che ck t o de t e rm ine
if t he Re sult Fragm e nt alre ady e xist s. If it do es exist, we se t t he re ve rse d St ring using t he
Re sult Fragm e nt 's se t Re ve rse T e xt () m e t ho d. If the Fragment can't be fo und in the FragmentManager, then we
st art a ne w Re sult Act ivit y, and pass t he re ve rse d St ring int o it using an e xt ras Bundle . We reuse the key
co nstant KEY_REVERSED_T EXT that we defined earlier to handle persisting data o n ro tatio n.
Next let's co nsider the alternate activity_main.xml layo ut we defined in the /re s/layo ut -sw6 0 0 dp fo lder:
OBSERVE: /res/layo ut-sw6 0 0 dp/activity_main.xml
...
<fragment
android:id="@+id/fragResult"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
class="com.ost.android.advancedlayouts.ResultFragment" />
</LinearLayout>
This layo ut fo lder has a qualifier o f sw600dp, which stands fo r "smallest width o f at least 6 0 0 dp." The smallest width
refers to the smallest dimensio n fo r a device in either landscape o r po rtrait. Pho nes have a relatively small "smallest
width" co mpared to tablets. 6 0 0 dp is a fairly standard cut-o ff line fo r the "smallest width" fo r a device to be co nsidered
a tablet. Since we have at least 6 0 0 dp width o f screen space, we cho o se to include t he Re sult Fragm e nt in o ur
layo ut in additio n to the o ther view co mpo nent we're using fo r o ur main view. This means pho nes will o nly lo ad the
default act ivit y_m ain.xm l fro m the layo ut o r layo ut -land fo lder, which do n't include the ResultFragment. So , o n
pho nes, MainAct ivit y wo n't be able to find the ResultFragment in its view, so it will launch the Re sult Act ivit y
instead.
Fro m here we'll jump into the ResultActivity class:
OBSERVE: ReverseActivity.java
public class ResultActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.result_activity);
Bundle extras = getIntent().getExtras();
String reversedText = extras.getString(MainActivity.KEY_REVERSED_TEXT);
ResultFragment f = (ResultFragment) getSupportFragmentManager().findFragmentById(R.
id.fragResult);
if (f != null) {
f.setReverseText(reversedText);
}
}
}
In the ResultActivity class, we need to re t rie ve t he re ve rse d St ring f ro m t he Int e nt e xt ras and then pass it t o
t he Re sult Fragm e nt . The ResultFragment was defined in the re sult _act ivit y.xm l layo ut so we o nly need to f ind
t he f ragm e nt using t he Fragm e nt Manage r and then call the same se t Re ve rse T e xt () metho d again. That's ho w
we're able to reuse the ResultFragment to acco mplish o ur go als fo r bo th Tablets and Pho nes.
Wrapping Up
We co vered a lo t o f gro und in this lesso n. We started by learning ho w reso urce qualifiers can be used to lo ad alternate
layo uts based o n device o rientatio n. Then we learned ho w to make sure o ur app persists its data during ro tatio n.
Finally, we learned ho w to co mbine alternate layo uts with Fragments to co nditio nally include an additio nal Fragment in
o ur view when the screen is large eno ugh, o r lo ad the view in an Activity if there isn't eno ugh space. With what yo u've
learned in this lesso n, yo u can create truly po werful applicatio ns that suppo rt bo th Tablet and Pho ne sized devices in
whatever o rientatio n the user prefers. Nice wo rk. See yo u in the next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Custom View Components
Lesson Objectives
In this lesso n yo u will:
create a custo m view co mpo nent fro m scratch.
include the custo m co mpo nent in a layo ut via XML.
define custo m attributes.
handle custo m attributes in a custo m co mpo nent.
use custo m attributes in XML layo uts.
We've do ne lo ts o f UI building using vario us co mpo nents pro vided by Andro id, but there will co me a time when yo u want to
create yo ur o wn reusable UI co mpo nent. This may be because yo u want to :
co mpletely custo mize the lo o k, layo ut, and behavio r o f a graphical co mpo nent.
change the behavio r o r appearance o f an existing co mpo nent.
co mbine several existing co mpo nents into a single reusable widget.
Defining a Custom Component
Create a new Andro id pro ject using this criteria:
Name the pro ject Cust o m Co m po ne nt s.
Use the package name co m .o re illyscho o l.andro id2.cust o m co m po ne nt s.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
In the new pro ject, create a new class named MyCust o m Co m po ne nt , set the package to
co m .o re illyscho o l.andro id2.cust o m co m po ne nt s, have it extend fro m andro id.widge t .Fram e Layo ut , and
check the Co nst ruct o rs f ro m supe rclass bo x. The co mpleted class will have three co nstructo rs:
MyCusto mCo mpo nent(Co ntext co ntext)
MyCusto mCo mpo nent(Co ntext co ntext, AttributeSet attrs)
MyCusto mCo mpo nent(Co ntext co ntext, AttributeSet attrs, int defStyle)
Mo dify MyCust o m Co m po ne nt .java as sho wn:
CODE TO TYPE: MyCusto mCo mpo nent.java
package com.oreillyschool.android2.customcomponents;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class MyCustomComponent extends FrameLayout {
public MyCustomComponent(Context context) {
super(context);
// TODO Auto-generated constructor stub
this(context, null);
}
public MyCustomComponent(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
this(context, attrs, 0);
}
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor
}
}
No w create a new Andro id XML file named m y_cust o m _co m po ne nt _vie w.xm l with a Line arLayo ut as the ro o t
element and make sure that the file is saved in the /res/layo ut fo lder. Then mo dify
m y_cust o m _co m po ne nt _vie w.xm l as sho wn:
CODE TO TYPE: /res/layo ut/my_custo m_co mpo nent_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="verticalhorizontal" >
<ToggleButton android:id="@+id/button01"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="One"
android:textOff="One" />
<ToggleButton android:id="@+id/button02"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="Two"
android:textOff="Two" />
<ToggleButton android:id="@+id/button03"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="Three"
android:textOff="Three" />
</LinearLayout>
No w, return to MyCust o m Co m po ne nt .java and make this change:
CODE TO TYPE: MyCusto mCo mpo nent.java
package com.oreillyschool.android2.customcomponents;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class MyCustomComponent extends FrameLayout {
public MyCustomComponent(Context context) {
this(context, null);
}
public MyCustomComponent(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.my_custom_component_view, this);
}
}
We've defined the custo m co mpo nent's class and layo ut. No w we'll add it to o ur applicatio n. Open
act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<com.oreillyschool.android2.customcomponents.MyCustomComponent
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLinearLayout>
Save the changes and run it in the emulato r:
This is a great start. No w add so me functio nality to make it mo re interesting. Go back to
MyCust o m Co m po ne nt .java and make these changes:
CODE TO TYPE: MyCusto mCo mpo nent.java
package com.oreillyschool.android2.customcomponents;
import
import
import
import
import
import
android.content.Context;
android.util.AttributeSet;
android.view.View;
android.view.View.OnClickListener;
android.widget.ToggleButton;
android.widget.FrameLayout;
public class MyCustomComponent extends FrameLayout implements OnClickListener {
private
private
private
private
ToggleButton
ToggleButton
ToggleButton
ToggleButton
button01;
button02;
button03;
selectedToggleButton;
public MyCustomComponent(Context context) {
this(context, null);
}
public MyCustomComponent(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.my_custom_component_view, this);
button01 = (ToggleButton)findViewById(R.id.button01);
button02 = (ToggleButton)findViewById(R.id.button02);
button03 = (ToggleButton)findViewById(R.id.button03);
button01.setChecked(true);
selectedToggleButton = button01;
button01.setOnClickListener(this);
button02.setOnClickListener(this);
button03.setOnClickListener(this);
}
@Override
public void onClick(View v) {
selectedToggleButton.setChecked(false);
selectedToggleButton = (ToggleButton)v;
selectedToggleButton.setChecked(true);
}
}
Save the changes and run the applicatio n. The co mpo nents behave like radio butto ns: o nly o ne butto n can be checked
at a time and each time a new butto n is clicked, the previo us o ne beco mes unchecked.
Great. No w that we have all that wo rking, let's go back and take a lo o k at what we've do ne.
OBSERVE: MyCusto mCo mpo nent.java
...
public class MyCustomComponent extends FrameLayout implements OnClickListener {
private
private
private
private
ToggleButton
ToggleButton
ToggleButton
ToggleButton
button01;
button02;
button03;
selectedToggleButton;
public MyCustomComponent(Context context) {
this(context, null);
}
public MyCustomComponent(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.my_custom_component_view, this);
button01 = (ToggleButton)findViewById(R.id.button01);
button02 = (ToggleButton)findViewById(R.id.button02);
button03 = (ToggleButton)findViewById(R.id.button03);
button01.setChecked(true);
selectedToggleButton = button01;
button01.setOnClickListener(this);
button02.setOnClickListener(this);
button03.setOnClickListener(this);
}
@Override
public void onClick(View v) {
selectedToggleButton.setChecked(false);
selectedToggleButton = (ToggleButton)v;
selectedToggleButton.setChecked(true);
}
}
We create a new co mpo nent by sub-classing an existing UI co mpo nent (andro id.widge t .Fram e Layo ut ) to create a
butto n bar. Instead o f creating a co mpletely custo m co mpo nent, we gro up existing co mpo nents to gether (in this case
three T o ggle But t o n instances: but t o n0 1, but t o n0 2 and but t o n0 3), and add custo m behavio r to ensure that o nly
o ne butto n is checked at a time in the butto n bar:
OBSERVE: my_custo m_co mpo nent_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<ToggleButton android:id="@+id/button01"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="One"
android:textOff="One" />
<ToggleButton android:id="@+id/button02"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="Two"
android:textOff=Two" />
<ToggleButton android:id="@+id/button03"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="Three"
android:textOff="Three" />
</LinearLayout>
We create a co rrespo nding layo ut fo r o ur custo m co mpo nent in m y_cust o m _co m po ne nt _vie w.xm l, that co nsists
o f three T o ggle But t o n no des inside o f a Line arLayo ut , which presents the butto ns ho rizo ntally and co mprises
o ur butto n bar:
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<com.oreillyschool.android2.customcomponents.MyCustomComponent
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Finally, we place o ur custo m co mpo nent inside a view by adding a
co m .o re illyscho o l.andro id2.cust o m co m po ne nt s.MyCust o m Co m po ne nt no de in act ivit y_m ain.xm l,
which creates an instance o f o ur co mpo nent inside the main view o f o ur applicatio n, and sets the width and height o f
that instance.
Implementing View Attributes in a Custom Component
To make o ur custo m co mpo nent feel mo re like an o fficial Andro id co mpo nent, we can add custo m attributes that allo w
us to specify certain pro perties within XML layo ut files rather than pro grammatically. This streamlines the co mpo nent
and makes it easier to use.
Create a new Andro id XML file. Specify Value s as the Reso urce Type and name the file at t rs.xm l. Make sure that the
file is saved to re s/value s. Then add this co de to at t rs.xm l:
CODE TO TYPE: attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyCustomComponent">
<attr format="integer" name="defaultButtonIndex" />
</declare-styleable>
</resources>
Next, mo dify MyCust o m Co m po ne nt .java as sho wn:
CODE TO TYPE: MyCusto mCo mpo nent
package com.oreillyschool.android2.customcomponents;
import
import
import
import
import
import
import
android.content.Context;
android.content.res.TypedArray;
android.util.AttributeSet;
android.view.View;
android.view.View.OnClickListener;
android.widget.ToggleButton;
android.widget.FrameLayout;
public class MyCustomComponent extends FrameLayout implements OnClickListener {
private
private
private
private
ToggleButton
ToggleButton
ToggleButton
ToggleButton
button01;
button02;
button03;
selectedToggleButton;
public MyCustomComponent(Context context) {
this(context, null);
}
public MyCustomComponent(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.my_custom_component_view, this);
button01 = (ToggleButton)findViewById(R.id.button01);
button02 = (ToggleButton)findViewById(R.id.button02);
button03 = (ToggleButton)findViewById(R.id.button03);
button01.setChecked(true);
selectedToggleButton = button01;
button01.setOnClickListener(this);
button02.setOnClickListener(this);
button03.setOnClickListener(this);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyCustomC
omponent, defStyle, 0);
final int N = a.getIndexCount();
for (int i=0; i<N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyCustomComponent_defaultButtonIndex:
int index = a.getInt(attr, 0);
switch(index) {
case 1:
selectedToggleButton = button02;
break;
case 2:
selectedToggleButton = button03;
break;
default:
selectedToggleButton = button01;
break;
}
selectedToggleButton.setChecked(true);
break;
}
}
a.recycle();
}
@Override
public void onClick(View v) {
selectedToggleButton.setChecked(false);
selectedToggleButton = (ToggleButton)v;
selectedToggleButton.setChecked(true);
}
}
Finally, mo dify act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:oreilly="http://schemas.android.com/apk/res/com.oreillyschool.android2.customco
mponents"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<com.oreillyschool.android2.customcomponents.MyCustomComponent
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
oreilly:defaultButtonIndex="2" />
</LinearLayout>
Save the changes and run the applicatio n; when the applicatio n lo ads, instead o f the first butto n, the third butto n is
checked.
Let's review what we've do ne. We add a custo m attribute to o ur custo m co mpo nent. This custo m attribute allo ws us to
specify the butto n within the butto n bar that will be checked by default when the co mpo nent first lo ads. Using a custo m
attribute allo ws us to co nfigure the co mpo nent within a layo ut (in this case within the main view o f o ur applicatio n), as
well as make the co mpo nent cleaner and even mo re reusable. We can also distinguish between the pro perties o ur
co mpo nent inherits fro m its parent class, and its o wn pro perties:
OBSERVE: attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyCustomComponent">
<attr format="integer" name="defaultButtonIndex" />
</declare-styleable>
</resources>
The values file at t rs.xm l ho lds the declaratio ns fo r custo m attributes. In o ur case, we specify that o ur custo m
co mpo nent, MyCust o m Co m po ne nt , is st yle able , then we specify an integer attribute, de f ault But t o nInde x. This
attribute is the zero -based index o f the butto n we want checked, by default:
OBSERVE: MyCusto mCo mpo nent.java
...
public MyCustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.my_custom_component_view, this);
button01 = (ToggleButton)findViewById(R.id.button01);
button02 = (ToggleButton)findViewById(R.id.button02);
button03 = (ToggleButton)findViewById(R.id.button03);
button01.setOnClickListener(this);
button02.setOnClickListener(this);
button03.setOnClickListener(this);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyCustomC
omponent, defStyle, 0);
final int N = a.getIndexCount();
for (int i=0; i<N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyCustomComponent_defaultButtonIndex:
int index = a.getInt(attr, 0);
switch(index) {
case 1:
selectedToggleButton = button02;
break;
case 2:
selectedToggleButton = button03;
break;
default:
selectedToggleButton = button01;
break;
}
selectedToggleButton.setChecked(true);
break;
}
}
a.recycle();
}
@Override
public void onClick(View v) {
selectedToggleButton.setChecked(false);
selectedToggleButton = (ToggleButton)v;
selectedToggleButton.setChecked(true);
}
}
Next, we add lo gic to MyCust o m Co m po ne nt to determine whether the default butto n index is specified and, if it is, to
set the default butto n. In o rder to do that, we retrieve an array o f all o f the styled attributes specified by the layo ut XML,
lo o k thro ugh the array to determne whether de f ault But t o nInde x is set, and then set the default checked butto n. We
se t a de f ault value , just in case de f ault But t o nInde x wasn't used in the layo ut.
We also make a call to re cycle () o n t he T ype dArray o bje ct after we finish reading the attributes. Andro id reuses
the reso urce array fo r multiple co mpo nents, so it's impo rtant to call this metho d whenever yo u finish reading the
values required by yo ur custo m co mpo nent:
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:oreilly="http://schemas.android.com/apk/res/com.oreillyschool.android2.customco
mponents"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<com.oreillyschool.android2.customcomponents.MyCustomComponent
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
oreilly:defaultButtonIndex="2" />
</LinearLayout>
Finally, we go to o ur applicatio n's layo ut and add a nam e space f o r o ur cust o m at t ribut e s (the full package name
o f o ur applicatio n). Then, using that namespace, we se t t he cust o m at t ribut e o n o ur cust o m co m po ne nt .
Wrapping Up
Whew! We co vered a lo t o f fairly advanced to pics. When yo u kno w ho w to create custo m co mpo nents pro perly in
Andro id, it o pens up a lo t o f o ptio ns in yo ur applicatio n develo pment. No w yo u aren't limited to basic Andro id
co mpo nents. No w when Andro id do esn't pro vide the co mpo nent yo u're lo o king fo r, yo u can just create yo ur o wn!
Go o d luck with the ho mewo rk and see yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Basic Services
Lesson Objectives
In this lesso n yo u will:
create an Andro id Service implementatio n fro m scratch.
perfo rm a lo ng running task in a Service.
define/register a Service in an applicatio n manifest.
start and sto p a service pro gramatically.
There are two main uses fo r an Andro id Service:
They allo w yo u to specify lo ng-running parts o f yo ur applicatio n to run in the backgro und, leaving the rest o f yo ur
applicatio n available fo r user interactio n. (Fo r example, if yo u had a media player applicatio n that co uld do wnlo ad
new co ntent, yo u wo uld want the media do wnlo ading to o ccur in a Service.)
They allo w yo u to specify functio nality in yo ur applicatio n that yo u can make available to o ther applicatio ns as well.
(Fo r example, yo u might have a Twitter client that expo ses its uplo ading/tweeting service to o ther applicatio ns so that
yo u can po st tweets o r images fro m them.)
The Andro id Develo per do cumentatio n clarifies that Services are not threads themselves, o r separate pro cesses, but rather a
means o f letting the system kno w what functio nality yo u want to run in the backgro und o r share with o ther applicatio ns, and that
the system itself takes charge o f the Service functio nality and its callbacks.
Creating, Declaring, and Starting a Service
Let's get started. Create a new Andro id pro ject with this criteria:
Name the pro ject BasicSe rvice s.
Use the package name co m .o re illyscho o l.andro id2.basicse rvice s.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
Let's start with o ur Service class. Create a new class in the co m .o re illyscho o l.andro id2.basicse rvice s package
that extends fro m andro id.app.Se rvice . Name it Sim ple Se rvice . Yo u see a stub fo r a single metho d:
o nBind(Int e nt arg0 ) (we'll change that to o nBind(Int e nt int e nt ).) Right-click the Sim ple Se rvice filename and
select So urce | Ove rride /Im ple m e nt Me t ho ds. A windo w appears sho wing metho ds in the Service parent class
that yo u can o verride. Select o nCre at e (), o nDe st ro y(), and o nSt art Co m m and(Int e nt , int , int ) and click OK.
No w yo u see stubs fo r tho se three metho ds as well.
Okay, make these changes:
CODE TO TYPE: SimpleService.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
android.app.Service;
android.content.Intent;
android.os.IBinder;
android.util.Log;
public class SimpleService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
Log.d("SimpleService", "Service created.");
}
@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
Log.d("SimpleService", "Service destroyed.");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Auto-generated method stub
Log.d("SimpleService", "Service started.");
return START_STICKY;
return super.onStartCommand(intent, flags, startId);
}
}
Add the co de belo w to Andro idManif e st .xm l:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.basicservices"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".SimpleService" />
<activity
android:name=".MainActivity"
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>
Finally, make these changes in MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.os.Bundle;
android.os.Handler;
android.view.Menu;
public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService(new Intent(MainActivity.this, SimpleService.class));
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopService(new Intent(MainActivity.this, SimpleService.class));
}
}, 10000);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Run the applicatio n to see o ur service start and sto p. Once it's lo aded into yo ur emulato r, take a lo o k at the Lo gCat
windo w in Eclipse:
The service was created, started, and eventually destro yed. Our service didn't do much, but we can lo o k at o ur co de
and see the general pro cedure fo r creating and starting services:
OBSERVE: SimpleService.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
android.app.Service;
android.content.Intent;
android.os.IBinder;
android.util.Log;
public class SimpleService extends Service {
@Override
public void onCreate() {
super.onCreate();
Log.d("SimpleService", "Service created.");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("SimpleService", "Service destroyed.");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("SimpleService", "Service started.");
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
The Andro id o perating system manages the actual Service instance. We implement callbacks fo r the creatio n,
destructio n, and start o f a service. In tho se callbacks, we add lo g st at e m e nt s so we can see when each metho d is
executed in Lo gCat.
OBSERVE: Andro idManifest.xml
<?xml version="1.0" encoding="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.basicservices"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="10" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<service android:name=".SimpleService" />
<activity
android:name=".MainActivity"
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>
Just like Activities, Se rvice s m ust be de clare d inside the manifest. This registers the Service with the Andro id OS so
that it can create an instance when it receives an Int e nt fo r the Service.
OBSERVE: MainActivity.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
android.app.Activity;
android.content.Intent;
android.os.Bundle;
android.os.Handler;
public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService(new Intent(MainActivity.this, SimpleService.class));
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopService(new Intent(MainActivity.this, SimpleService.class));
}
}, 10000);
}
}
In the main activity, we start the service, and then schedule it to sto p 10 seco nds later, using a Handler o bject's
po st De laye d(Runnable , lo ng) metho d. The Handler allo ws us to execute so me co de after a delay. The Runnable
o bject's run() metho d is executed after the delay. The delay is defined by the seco nd parameter, a lo ng, which is
interpreted as milliseco nds. The metho d's st art Se rvice and st o pSe rvice are bo th defined o n the Co nt e xt class,
which is a parent class o f Act ivit y. These metho ds take an Intent, which is simliar to the startActivity metho d that we
use to start a new Activity, but this Intent gives a reference to o ur Sim ple Se rvice .class type instead. In this example,
we see the basic pro cedure to create, declare, start, and sto p a service. Befo re we mo ve o n, let's specify a task fo r
service to execute.
Right-click the re s fo lder in yo ur BasicSe rvice s ro o t pro ject fo lder and select Ne w | Fo lde r. In the New Fo lder
windo w, create a fo lder named raw. No w we'll add an audio file to o ur pro ject to integrate into o ur applicatio n. To
do wnlo ad the audio file, right-click o n the link belo w and save the file to yo ur "Co mputer (\\beam\winusers) (V:)"
wo rkspace /BasicSe rvice s/re s/raw fo lder.
Do wnlo ad the audio file here: art_no w_by_alex_bero za.mp3
("Art No w" by Alex (feat. Sno wflake) is licensed under a Creative Co mmo ns license.)
Note
The /raw fo lder is used primarily fo r sto ring files within yo ur applicatio n in their raw unco mpressed fo rm.
Files in the /raw fo lder are given a reso urce id in the fo rm o f R.raw.filename. Typically, they are accessed
in co de via the Reso urces.o penRawReso urce() metho d.
No w make these changes to Sim ple Se rvice :
CODE TO TYPE: SimpleService.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
import
android.app.Service;
android.content.Intent;
android.media.MediaPlayer;
android.os.IBinder;
android.util.Log;
public class SimpleService extends Service {
private MediaPlayer mPlayer;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mPlayer = MediaPlayer.create(this, R.raw.art_now_by_alex_beroza);
Log.d("SimpleService", "Service created.");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("SimpleService", "Service destroyed.");
mPlayer.stop();
mPlayer = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("SimpleService", "Service started.");
mPlayer.start();
return START_STICKY;
}
}
Let's make a few changes to the interfaces. First, make these changes to /re s/value s/st rings.xm l:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World, MainActivity!</string><string>
<string name="app_name">BasicServices</string>
<string name="start_button_label">Start</string>
<string name="stop_button_label">Stop</string>
<string name="attribution_text">"Art Now" by Alex (feat. Snowflake)\nhttp://ccmixte
r.org/files/AlexBeroza/30344\nis licensed under a Creative Commons license:\nhttp://cre
ativecommons.org/licenses/by/3.0/</string>
</resources>
Next, make these changes to act ivit y_m ain.xm l in re s/layo ut :
CODE TO TYPE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/startButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_button_label" />
<Button
android:id="@+id/stopButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/stop_button_label" />
</LinearLayout>
<TextView
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
android:text="@string/attribution_text" />
</LinearLayout>
Finally, make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.os.Bundle;
android.os.Handler;
android.view.View;
android.view.View.OnClickListener;
public class MainActivity extends Activity {
private OnClickListener startOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
startService(new Intent(MainActivity.this, SimpleService.class));
}
};
private OnClickListener stopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stopService(new Intent(MainActivity.this, SimpleService.class));
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService(new Intent(MainActivity.this, SimpleService.class));
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopService(new Intent(MainActivity.this, SimpleService.class));
}
}, 10000);
findViewById(R.id.startButton).setOnClickListener(startOnClickListener);
findViewById(R.id.stopButton).setOnClickListener(stopOnClickListener);
}
}
Save the mo dified files and run the applicatio n. When yo u click the St art butto n, the audio file starts playing. The audio
is playing in the backgro und, so if yo u click the ho me butto n o n yo ur emulato r, the audio will co ntinue to play even
tho ugh the applicatio n is no t in the fo regro und. If yo u go back to the applicatio n and click St o p, the audio will sto p.
Let's take a lo o k at o ur changes:
OBSERVE: SimpleService.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
android.app.Service;
android.content.Intent;
android.media.MediaPlayer;
android.os.IBinder;
public class SimpleService extends Service {
private MediaPlayer mPlayer;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mPlayer = MediaPlayer.create(this, R.raw.art_now_by_alex_beroza);
}
@Override
public void onDestroy() {
super.onDestroy();
mPlayer.stop();
mPlayer = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mPlayer.start();
return START_STICKY;
}
}
In Sim ple Se rvice , we add a Me diaPlaye r and call st art () when the service starts and st o p() when the Service
sto ps (and is destro yed). This is a basic implementatio n o f the Me diaPlaye r o bject. It co uld certainly be impro ved, but
that's o utside o f the sco pe o f this lesso n. We'll go o ver audio and video playback in a future lesso n.
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/startButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_button_label" />
<Button
android:id="@+id/stopButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/stop_button_label" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/attribution_text" />
</LinearLayout>
In the activity_main.xml layo ut, we add two butto ns fo r st art ing and st o pping the Service:
OBSERVE: MainActivity.java
package com.oreillyschool.android2.basicservices;
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.os.Bundle;
android.view.View;
android.view.View.OnClickListener;
public class MainActivity extends Activity {
private OnClickListener startOnClickListener = newOnClickListener() {
@Override
public void onClick(View v) {
startService(new Intent(MainActivity.this, SimpleService.class));
}
};
private OnClickListener stopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stopService(new Intent(MainActivity.this, SimpleService.class));
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.startButton).setOnClickListener(startOnClickListener);
findViewById(R.id.stopButton).setOnClickListener(stopOnClickListener);
}
}
Finally, in the MainActivity.java class, we wire the butto ns in the interface to start and sto p the Service and thereby start
and sto p the music.
Let's co nsider o ur Se rvice class o nce again, specifically the return value o f o nSt art Co m m and. There are a few
different mo des o f o peratio n fo r an Andro id Service. By specifying a particular value fo r o nSt art Co m m and to return,
yo u specify the mo de in which the service will run. In o ur example, we specify the co nstant value ST ART _ST ICKY. In
do ing so , we let the system kno w that if o ur SimpleService is killed (usually due to the system needing to free up
memo ry when memo ry is lo w) after o nSt art Co m m and(), then the system sho uld restart it. This value allo ws a
service to be started and run indefinitely until yo u explicitly sto p it. In o ur example, the music file will play in the
backgro und indefinitely (o r until the entire file finishes playing). Services that behave in this way are generally referred to
as started services. The suppo rted co nstants fo r the o nSt art Co m m and result, defined o n the Service class, are
START_STICKY, START_NOT_STICKY, START_REDELIVER_INTENT, and START_STICKY_COMPATIBILITY. Fo r a
detailed descriptio n abo ut ho w Service will behave based o n each co nstant, read the Andro id Develo per
Do cumentatio n site linked to each co nstant we mentio ned.
If we want o ur service to run o nly when we have specific tasks (Intents) fo r it to handle, then we specify
o nSt art Co m m and to return ST ART _NOT _ST ICKY. In this case, if the service is killed after o nSt art Co m m and(),
it is no t restarted unless there are Intents waiting to be handled. This behavio r is useful fo r pro cessing wo rk
independently, but there is ano ther to o l we can use to do the same wo rk mo re efficiently: an extensio n o f the Se rvice
class called Int e nt Se rvice .
The Int e nt Se rvice class is an extremely useful subclass o f Se rvice . Because yo u o nly need to implement o ne
abstract metho d (o nHandle Int e nt ), it's easier to use than a regular Service. By default, the Int e nt Se rvice class
uses a ST ART _NOT _ST ICKY co mmand mo de type, so this class is a great o ptio n if yo u need to perfo rm wo rk in a
Service, but do n't want to implement a full Se rvice class. Ano ther benefit o f Int e nt Se rvice is that the
o nHandle Int e nt metho d is executed in a separate wo rker thread, so yo u do n't have to wo rry abo ut accidentally
perfo rming wo rk o n yo ur applicatio n's primary thread.
Wrapping Up
Services are an essential to o l o f the Andro id SDK. In this lesso n, we learned abo ut creating a basic Se rvice class
implementatio n. We co vered ho w to sto p and start a service fro m o ur Applicatio n, and learned a bit abo ut ho w the life
cycle o f a Service can be affected by the o nSt art Co m m and return value. We also learned abo ut a co nvenient
subclass alternative to Se rvice called Int e nt Se rvice . Get co zy and co mfo rtable creating and using Services in yo ur
o wn applicatio ns. Practice in the ho mewo rk and see yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Notifications
Lesson Objectives
In this lesso n yo u will:
create a No tificatio n.
start a No tificatio n using an Intent.
implement actio ns to be perfo rmed when an Intent is clicked.
pro gramatically update a No tificatio n.
pro gramatically remo ve a No tificatio n.
In the previo us lesso n, we used Andro id Services to run tasks in the backgro und. Running tasks in the backgro und allo ws o ur
applicatio n to do o ther wo rk while waiting fo r the task to finish. When we want a running service to let us kno w when certain
events o ccur, we can use No tificatio ns. No tificatio ns info rm the user o f events, as well as pro vide a means by which to launch
Activities fro m applicatio ns.
Creat and Update a Notification
Let's get started. Create a new Andro id pro ject using this criteria:
Name the pro ject No t if icat io ns.
Use the package name co m .o re illyscho o l.andro id2.no t if icat io ns.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
Open /re s/layo ut /act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<Button
android:id="@+id/button_notify_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notify Now" />
</RelativeLinearLayout>
Open the MainAct ivit y.java class in the /src fo lder and make these changes:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.notifications;
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.app.Notification;
android.app.NotificationManager;
android.app.PendingIntent;
android.content.Intent;
android.os.Bundle;
android.support.v4.app.NotificationCompat;
android.support.v4.app.TaskStackBuilder;
android.view.Menu;
android.view.View;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button_notify_now).setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
notifyNow();
}
});
}
public void notifyNow() {
PendingIntent pi = TaskStackBuilder.create(this)
.addParentStack(MainActivity.class)
.addNextIntent(new Intent())
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("Notify now")
.setContentText("You've been notified!")
.setContentIntent(pi)
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.notify(0, notification);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Note
Make sure yo u impo rt the andro id.suppo rt .v4 .app.T askSt ackBuilde r versio n o f the
T askSt ackBuilde r here, o therwise the applicatio n will crash o n pre-Ice Cream Sandwich devices.
That's it! No w we're ready to test o ur first no tificatio n. Run the applicatio n and yo u'll see a simple layo ut with a single
butto n. Click the butto n and yo u see a new no tificatio n at the to p o f the screen.
If yo u click and drag do wn fro m anywhere o n that bar at the to p o f the screen yo u'll o pen up the no tificatio n drawer.
Here yo u'll be able to see the actual no tificatio n we just created (instead o f just the ico n).
Yo u're o ff to a great start. Befo re yo u go any further, let's lo o k o ver this co de and analyze it:
OBSERVE: MainActivity.java
public void notifyNow() {
PendingIntent pi = TaskStackBuilder.create(this)
.addParentStack(MainActivity.class)
.addNextIntent(new Intent())
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
The no t if yNo w() metho d is executed whenever we press the butto n in o ur view. The first o bject we create is a
Pe ndingInt e nt . The Pe ndingInt e nt is used to instruct the Andro id System ho w to react when the user taps the
No tificatio n in the no tificatio n drawer. We co nstruct Pe ndingInt e nt using the T askSt ackBuilde r class, which uses
the builder pattern to help generate o ur PendingIntent. In o rder to use it in the no tificatio n, we must define a "parent
stack" by calling addPare nt St ack(MainAct ivit y.class) and the "next Intent" using addNe xt Int e nt (ne w Int e nt ())
with the builder, befo re generating the final Pe ndingInt e nt with ge t Pe ndingInt e nt (0 ,
Pe ndingInt e nt .FLAG_UPDAT E_CURRENT ).
The addPare nt St ack metho d requires a parameter reference to an Act ivit y class o r o bject, so here we can just use
the keywo rd t his. If we were making a no tificatio n fro m within a Service, we wo uld use a class reference instead, such
as MainAct ivit y.class. The addNe xt Int e nt metho d requires an Intent as its o nly parameter. This Int e nt will start
when the user taps the No tificatio n. We send a new, plain Int e nt o bject that do esn't launch anything when it's
executed. The final metho d, ge t Pe ndingInt e nt , requires at least two parameters: an Integer request co de parameter
that can be attached to the intent, and an Integer flag that defines ho w the system sho uld handle o ther PendingIntents
with the same no tificatio n id that it receives fro m o ur app. The Pe ndingInt e nt .FLAG_UPDAT E_CURRENT flag,
retains any PendingIntent that matches o ur no tificatio n id and replaces its extra data with the data attached to this new
PendingIntent. This no tificatio n id is referenced later, and is no t related to the requestCo de we just defined.
OBSERVE:
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("Notify now")
.setContentText("You've been notified!")
.setContentIntent(pi)
.build();
After co nstructing Pe ndingInt e nt , we have to co nstruct an actual No t if icat io n o bject. We use a builder to do this as
well. We use the No t if icat io nCo m pat .Builde r class reference so o ur no tificatio n wo rks o n pre-Ice Cream
Sandwich devices. We have fo ur metho ds we must call here befo re building the final no tificatio n. First, se t Sm allIco n
defines the ico n that is used in the no tificatio n drawer. se t Co nt e nt T it le and se t Co nt e nt T e xt define the title and
co ntent o f the no tificatio n, respectively. Then we call se t Co nt e nt Int e nt with the Pe ndingInt e nt we generated
earlier to register the intent with o ur no tificatio n. The final metho d, build(), takes no parameters and simply finalizes
the builder, returning the generated no tificatio n o bject.
OBSERVE:
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.notify(0, notification);
Finally, we find a reference to the device's No t if icat io nManage r using the ge t Syst e m Se rvice () metho d, passing
the NOT IFICAT ION_SERVICE co nstant, and casting the result to the pro pe r class t ype . We call the no t if y metho d,
passing a no t if icat io n id which acts as the identifier fo r the no tificatio n unique within o ur app, and the ge ne rat e d
no t if icat io n o bje ct as well.
Responding T o User T aps On A Notification
Let's update o ur co de so it actually do es so mething when we tap o ur no tificatio n. As yo u might have guessed, we
need to define a different "next Intent" fo r o ur Pe ndingInt e nt . Befo re we do that tho ugh, we'll make a new Activity that
we want to have launched when a user taps the no tificatio n. Create a new class named Ne xt Act ivit y, and make sure
the package name is the same as the o thers (co m .o re illyscho o l.andro id2.no t if icat io ns). Also make sure to set
the superclass as andro id.app.Act ivit y. No w, make these changes to the class:
CODE TO TYPE: NextActivity.java
package com.oreillyschool.android2.notifications;
import android.app.Activity;
import android.app.NotificationManager;
import android.os.Bundle;
public class NextActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.cancel(0);
}
}
We referenced a layo ut in this co de that do esn't exist yet. Let's create it no w. In the /re s/layo ut fo lder, make a new
XML layo ut file named act ivit y_ne xt .xm l and then make these changes:
CODE TO TYPE: /res/layo utactivity_next.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Next Activity" />
</LinearLayout>
Do n't fo rget to add a reference to o ur new Activity to the Andro idManif e st .xm l:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.notifications"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.oreillyschool.android2.notifications.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".NextActivity"
android:label="NextActivity"/>
</application>
</manifest>
Finally, make these changes to MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.notifications;
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.app.Notification;
android.app.NotificationManager;
android.app.PendingIntent;
android.content.Intent;
android.os.Bundle;
android.support.v4.app.NotificationCompat;
android.support.v4.app.TaskStackBuilder;
android.view.View;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button_notify_now).setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
notifyNow();
}
});
}
public void notifyNow() {
Intent resultIntent = new Intent(this, NextActivity.class);
PendingIntent pi = TaskStackBuilder.create(this)
.addParentStack(this)
.addNextIntent(new Intent())
.addNextIntent(resultIntent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("Notify now")
.setContentText("You've been notified!")
.setContentIntent(pi)
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.notify(0, notification);
}
}
Run the pro gram. The o riginal butto n wo rks creates a no tificatio n that lo o ks exactly the same as befo re. Ho wever,
clicking the no tificatio n no w causes o ur new Activity to appear, and remo ves the no tificatio n fro m the no tificatio n
drawer:
Let's go o ver o ur changes, starting with MainAct ivit y.java:
OBSERVE: MainActivity.java
public void notifyNow() {
Intent resultIntent = new Intent(this, NextActivity.class);
PendingIntent pi = TaskStackBuilder.create(this)
.addParentStack(this)
.addNextIntent(resultIntent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("Notify now")
.setContentText("You've been notified!")
.setContentIntent(pi)
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE)
;
nm.notify(0, notification);
}
No t much changed here. We replace the Intent that we passed to addNe xt Int e nt () with the new re sult Int e nt created
at the beginning o f this metho d. This new re sult Int e nt references o ur newly created Ne xt Act ivit y class. No w when
the no tificatio n is clicked, the Ne xt Act ivit y will be launched, just as if we had called startActivity(resultIntent).
OBSERVE: NextActivity.java
package com.oreillyschool.android2.notifications;
import android.app.Activity;
import android.app.NotificationManager;
import android.os.Bundle;
public class NextActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.cancel(0);
}
}
Our new Activity isn't particulary unique, ho wever we did add so me co de to make sure the no tificatio n gets remo ved
fro m the no tificatio n drawer. We go t a reference to the No t if icat io nManage r just like we did in MainActivity.java, but
this time we call the cance l metho d with a value o f 0 . This remo ves any no tificatio n fro m the no tificatio n drawer
created by o ur applicatio n with a no tificatio n id o f 0 . We hard-co de o ur id in MainActivity.java so we can safely assume
the same id is in this class, ho wever, the id co uld also be passed thro ugh Intent extras if the precise id wasn't a hardco ded value.
Updating A Notification
Let's make o ne last change to o ur applicatio n to demo nstrate ho w to update a No tificatio n. Make these changes in
Ne xt Act ivit y.java:
CODE TO TYPE: NextActivity.java
package com.oreillyschool.android2.notifications;
import
import
import
import
import
import
import
import
android.app.Activity;
android.app.Notification;
android.app.NotificationManager;
android.app.PendingIntent;
android.content.Intent;
android.os.Bundle;
android.support.v4.app.NotificationCompat;
android.support.v4.app.TaskStackBuilder;
public class NextActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
Intent resultIntent = new Intent(this, MainActivity.class);
PendingIntent pi = TaskStackBuilder.create(this)
.addParentStack(this)
.addNextIntent(resultIntent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("Next Notify")
.setContentText("You've been re-notified!")
.setContentIntent(pi)
.build();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVIC
E);
nm.notify(0, notification);
nm.cancel(0);
}
}
That's it! Save and run the applicatio n o nce mo re to see the results. After tapping the no tificatio n o nce yo u see that the
the Ne xt Act ivit y is created. Drag the no tificatio n drawer back do wn and tap the no tificatio n o nce mo re, and yo u see
the MainAct ivit y again. This Activity is a new instance o f MainAct ivit y, no t the same o ne as befo re. Yo u can test this
by hitting the back butto n; yo u'll be taken back to the previo us Ne xt Act ivit y. Tap it o nce mo re to return back to the
o riginal Ne xt Act ivit y.
Wrapping Up
We've demo nstrated lo ts o f ways to interact with No tificatio ns in Andro id. We learned ho w to create no tificatio ns using
the T askSt ackBuilde r and No t if icat io nCo m pat .Builde r builder classes, as well as display, update, and remo ve
no tificatio ns using the No t if icat io nManage r. No tificatio ns have undergo ne big changes in recent versio ns o f the
Andro id SDK and many mo re features are available to devices running tho se versio ns o f Andro id. While we co uldn't
co ver everything in o ne lesso n, the skills yo u learned here will allo w yo u to create co nsistent no tificatio ns o n o ld and
new Andro id devices. Yo u'll get a chance to use yo ur new skills in the ho mewo rk. See yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Content Providers
Lesson Objectives
In the lesso n yo u will:
create a Co ntentPro vider fro m scratch.
create a database using SQLiteOpenHelper.
access Co ntentPro vider data thro ugh the Co ntentReso lver.
insert new data into a Co ntentPro vider.
display Co ntentPro vider results in a list using Curso r and Curso rAdapter.
In this lesso n, we'll co ver an Andro id feature called Co ntent Pro viders. Co ntent Pro viders encapsulate an applicatio n's
interactio n with structured data sto red o n the device. They pro vide simple Create, Read, Update, and Delete (o ften called CRUD)
metho ds, access to external Applicatio n sharing, and security. A Co ntent Pro vider is typically backed by a SQLite database
instance, but the actual implementatio n is co ntro lled by the develo per.
Creating and Using a Content Provider
We have a lo t o f co de to write befo re we'll be able to test this applicatio n, so let's get started. Create a new Andro id
pro ject with these criteria:
Name the pro ject Co nt e nt Pro vide rs.
Use the package name co m .o re illyscho o l.andro id2.co nt e nt pro vide rs.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
Uncheck the Cre at e cust o m launche r ico n bo x.
No w let's wo rk with o ur views. Open the act ivit y_m ain.xm l layo ut file and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Data:" />
<EditText
android:id="@+id/data_label_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:hint="Label"
android:imeOptions="actionNext"
android:inputType="text" />
<EditText
android:id="@+id/data_value_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Value"
android:imeOptions="actionDone"
android:inputType="number" />
<Button
android:id="@+id/add_data_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add" />
</LinearLayout>
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:choiceMode="singleChoice" />
</RelativeLinearLayout>
Next, create a new layo ut xml that we'll use fo r o ur list items. Create a new Andro id Layo ut XML file in the /re s/layo ut
fo lder named it e m _dat a.xm l, then make these changes.
CODE TO TYPE: item_data.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parentwrap_content"
android:orientation="verticalhorizontal" >
<CheckedTextView
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkMark="?android:attr/listChoiceIndicatorSingle" />
<TextView
android:id="@+id/id_label_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1" />
<TextView
android:id="@+id/data_label_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="2" />
<TextView
android:id="@+id/value_label_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_weight="1" />
</LinearLayout>
Next, we'll define a small data mo del fo r o ur Co ntent Pro vider. In the
co m .o re illyscho o l.andro id2.co nt e nt pro vide rs package, create a new Java class named MyDat aCo nt ract s,
and make the fo llo wing changes to the class:
CODE TO TYPE: MyDataCo ntracts.java
package com.oreillyschool.android2.contentproviders;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;
public final class MyDataContracts {
public static final String AUTHORITY = "com.oreillyschool.android2.contentproviders
.provider";
public static final Uri BASE_CONTENT_URI = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY)
.build();
public static final class DataContract implements BaseColumns {
/**
* The DataContract table name and MIME type vendor name
*/
public static final String NAME = "data";
/**
* The row name for the label field
*/
public static final String LABEL = "label";
/**
* The row name for the value field
*/
public static final String VALUE = "value";
public static final Uri DATA_CONTENT_URI = BASE_CONTENT_URI.buildUpon()
.appendPath(NAME).build();
}
}
No w create the actual Co ntent Pro vider, and a database helper. In the same package, create ano ther Java class
named MyDat aCo nt e nt Pro vide r and make the fo llo wing changes:
CODE TO TYPE: MyDataCo ntentPro vider.java
package com.oreillyschool.android2.contentproviders;
import
import
import
import
import
import
import
import
import
import
import
import
android.content.ContentProvider;
android.content.ContentResolver;
android.content.ContentUris;
android.content.ContentValues;
android.content.Context;
android.content.UriMatcher;
android.database.Cursor;
android.database.sqlite.SQLiteDatabase;
android.database.sqlite.SQLiteOpenHelper;
android.net.Uri;
android.provider.BaseColumns;
android.text.TextUtils;
import com.oreillyschool.android2.contentproviders.MyDataContracts.DataContract;
public class MyDataContentProvider extends ContentProvider {
private static final int B_DATA = 100;
private static final int B_DATA_ID = 101;
private static final String TYPE_DIR = "vnd.android.cursor.dir/vnd.com.oreillyschoo
l.android2.contentproviders.provider.%s";
private static final String TYPE_ITEM = "vnd.android.cursor.item/vnd.com.oreillysch
ool.android2.contentproviders.provider.%s";
private static final UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(MyDataContracts.AUTHORITY, DataContract.NAME, B_DATA);
sUriMatcher.addURI(MyDataContracts.AUTHORITY, DataContract.NAME+"/#", B_DATA_ID
);
}
private MySQLHelper mDBHelper;
@Override
public boolean onCreate() {
mDBHelper = new MySQLHelper(getContext());
return true;
}
@Override
public String getType(Uri uri) {
int uriCode = sUriMatcher.match(uri);
switch (uriCode) {
case B_DATA:
return String.format(TYPE_DIR, DataContract.NAME);
case B_DATA_ID:
return String.format(TYPE_ITEM, DataContract.NAME);
default:
throw new UnsupportedOperationException("Uri Not Supported");
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName)) {
long id = db.insert(tableName, null, values);
if (id > -1) {
ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(uri, null);
}
return ContentUris.withAppendedId(uri, id);
} else {
return null;
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] select
ionArgs, String sortOrder) {
SQLiteDatabase db = mDBHelper.getReadableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName)) {
Cursor cursor = db.query(tableName, projection, selection, selectionArgs, n
ull, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
} else {
return null;
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selecti
onArgs) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName))
return db.update(tableName, values, selection, selectionArgs);
else
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName))
return db.delete(tableName, selection, selectionArgs);
else
return 0;
}
private class MySQLHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "MySqliteDB";
private static final int DB_VERSION = 1;
private static final String CREATE_TABLE_DATA = "CREATE TABLE " + DataContract.
NAME + " ("
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ DataContract.LABEL + " STRING, "
+ DataContract.VALUE + " INTEGER)";
public MySQLHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_DATA);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DataContract.NAME);
onCreate(db);
}
public String getTableNameForCode(int uriCode) {
switch (uriCode) {
case B_DATA:
case B_DATA_ID:
return DataContract.NAME;
default:
return null;
}
}
}
}
No w, mo dify MainAct ivit y.java to tie everything to gether. Make these changes in the class:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.contentproviders;
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.content.ContentResolver;
android.content.ContentValues;
android.database.Cursor;
android.os.Bundle;
android.support.v4.app.FragmentActivity;
android.support.v4.app.LoaderManager;
android.support.v4.content.CursorLoader;
android.support.v4.content.Loader;
android.support.v4.widget.SimpleCursorAdapter;
android.text.TextUtils;
android.view.KeyEvent;
android.view.Menu;
android.view.View;
android.view.inputmethod.EditorInfo;
android.widget.Button;
android.widget.EditText;
android.widget.ListView;
android.widget.TextView;
android.widget.TextView.OnEditorActionListener;
import com.oreillyschool.android2.contentproviders.MyDataContracts.DataContract;
public class MainActivity extends FragmentActivity {
private
private
private
private
private
private
EditText mDataLabelEdit;
EditText mDataValueEdit;
Button mAddButton;
ListView mListView;
SimpleCursorAdapter mAdapter;
String[] mProjection;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDataLabelEdit = (EditText) findViewById(R.id.data_label_edit);
mDataValueEdit = (EditText) findViewById(R.id.data_value_edit);
mAddButton = (Button) findViewById(R.id.add_data_button);
mListView = (ListView) findViewById(android.R.id.list);
mProjection = new String[]{DataContract._ID, DataContract.LABEL, DataContract.V
ALUE};
int[] viewIds = {R.id.id_label_text, R.id.data_label_text, R.id.value_label_tex
t};
mAdapter = new SimpleCursorAdapter(this, R.layout.item_data, null, mProjection,
viewIds, 0);
mListView.setAdapter(mAdapter);
LoaderManager lm = getSupportLoaderManager();
lm.initLoader(0, null, mLoaderCallbacks);
mAddButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addNewData();
}
});
mDataValueEdit.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
addNewData();
return true;
}
return false;
}
});
}
private void addNewData() {
ContentValues values = new ContentValues();
String label = mDataLabelEdit.getText().toString();
String value = mDataValueEdit.getText().toString();
if (!TextUtils.isEmpty(label) && !TextUtils.isEmpty(value)) {
values.put(DataContract.LABEL, label);
values.put(DataContract.VALUE, value);
}
ContentResolver resolver = getContentResolver();
resolver.insert(DataContract.DATA_CONTENT_URI, values);
// Clear the old data
mDataLabelEdit.getText().clear();
mDataValueEdit.getText().clear();
mDataLabelEdit.requestFocus();
}
private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderManager.
LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(MainActivity.this, DataContract.DATA_CONTENT_URI,
mProjection, null, null, DataContract.VALUE + " ASC");
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Befo re we can test the App we have to make o ne last change. Just like Activities, Co ntentPro viders need to be defined
in the manifest. Open the pro ject's Andro idManif e st .xm l file and make these changes:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.contentproviders"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.oreillyschool.android2.contentproviders.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="com.oreillyschool.android2.contentproviders.MyDataContentProv
ider"
android:authorities="com.oreillyschool.android2.contentproviders.provider"
android:exported="false"
android:label="@string/app_name" />
</application>
</manifest>
No w we are able to test the applicatio n. Launch the applicatio n in the emulato r:
Type a text label and a numeric value in the fields at the to p and click Add to add yo ur entries to the list. They appear
immediately in the list. Add a few mo re entries with different values to see ho w the list is so rted o n the Values co lumn:
Examining the Code
We've written lo ts o f new co de to go o ver. Let's start with the "Co ntracts" class, MyDat aCo nt ract s:
OBSERVE: MyDataCo ntracts.java
package com.oreillyschool.android2.contentproviders;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;
public final class MyDataContracts {
public static final String AUTHORITY = "com.oreillyschool.android2.contentpr
oviders.provider";
public static final Uri BASE_CONTENT_URI = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY)
.build();
public static final class DataContract implements BaseColumns {
/**
* The DataContract table name and MIME type vendor name
*/
public static final String NAME = "data";
/**
* The row name for the label field
*/
public static final String LABEL = "label";
/**
* The row name for the value field
*/
public static final String VALUE = "value";
public static final Uri DATA_CONTENT_URI = BASE_CONTENT_URI.buildUpon()
.appendPath(NAME).build();
}
}
When implementing a Co ntent Pro vider, it can be useful to create a "Co ntracts" type class that can gro up
to gether relevant metadata co nstants that interact with yo ur Co ntent Pro vider. Go o gle uses Co ntract classes
fo r its shared Co ntent Pro viders, such as the Co ntacts Co ntent Pro vider.
The AUT HORIT Y variable is the symbo lic name o f the pro vider. It's used to register the pro vider with the
Andro id OS. It is also used as the base autho rity parameter o f all Query URIs that the pro vider suppo rts.
BASE_CONT ENT _URI is used (as the name implies) as the base part o f all URIs that the pro vider suppo rts.
We use a builder Uri.Builde r to help us co nstruct all URIs in the applicatio n. The
.sche m e (Co nt e nt Re so lve r.SCHEME_CONT ENT ) is required fo r Co ntent Pro viders. The variable passed
to the .aut ho rit y(AUT HORIT Y) part must match the autho rity value defined in the manifest. These are all
used by the OS to match Co ntent requests with yo ur Co ntent Pro vider.
We define an inner-class, Dat aCo nt ract , fo r o ur single "Data" table. It's co mmo n practice to have an innerclass defined in the Co ntract class fo r each table suppo rted by the pro vider. Here we have a co nstant defined
fo r the table name, NAME, and fo r each ro w o f o ur Data table: LABEL and VALUE. Our co ntract class also
implements the Base Co lum ns interface, which has no metho ds, but do es have a co nstant _ID that maps to
the "id" primary key co lumn o f o ur table required fo r each table in SQLite.
We also define the co nstant DAT A_CONT ENT _URI URI o bject that will map to o ur "data" table. It builds
upo n the BASE_CONT ENT _URI co nstant and appends its unique table name as a URI path.
No w let's go o ver the actual Co ntent Pro vider itself, MyDat aCo nt e nt Pro vide r. We'll fo cus o n a small piece
at a time, starting with the static variables defined at the to p o f the class:
OBSERVE: MyDataCo ntentPro vider.java - Part 1
public class MyDataContentProvider extends ContentProvider {
private static final int B_DATA = 100;
private static final int B_DATA_ID = 101;
private static final String TYPE_DIR = "vnd.android.cursor.dir/vnd.com.oreil
lyschool.android2.contentproviders.provider.%s";
private static final String TYPE_ITEM = "vnd.android.cursor.item/vnd.com.ore
illyschool.android2.contentproviders.provider.%s";
private static final UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(MyDataContracts.AUTHORITY, DataContract.NAME, B_DATA)
;
sUriMatcher.addURI(MyDataContracts.AUTHORITY, DataContract.NAME+"/#", B_
DATA_ID);
}
...
Here we define a few mo re metadata co nstants to help us match URIs to the appro priate Co ntract pro perties.
The first two co nstants, B_DAT A and B_DAT A_ID, are used fo r queries to o ur "Data" table; the first is used
to query a list o f results, while the seco nd is used to query a single result.
The T YPE_DIR and T YPE_IT EM co nstants are used to help define the MIME type o f the data returned fo r a
specific URI. Like the first two co nstants, these are used to distinguish between queries fo r multiple ro ws and
queries fo r a single ro w, respectively. In particular, the first half o f bo th o f these strings,
vnd.andro id.curso r.dir and vnd.andro id.curso r.it e m , are the Andro id-specific MIME types required in
Andro id in o rder to define a multi-ro w query and single ro w query. The last half o f the string (everything after
the "/" character) is called the subtype o f the MIME type. Usually subtypes are defined using the vendo r prefix
"vnd," the autho rity o f the co ntent pro vider, and the table name.
Note
While it is co mmo n practice to define Subtypes as we have in this applicatio n, it isn't required.
Fo r example, many Andro id built-in applicatio n Co ntent Pro viders use simplified subtypes. The
Co ntacts Co ntent Pro vider uses a subtype o f just phone_v2, which creates the entire MIME type
fo r a single ro w query vnd.andro id.curso r.it e m /pho ne _v2. Typically, MIME types are defined
as part o f the Co ntract class, but this isn't required. We define these here to relate them to the
ge t T ype () metho d in a mo re straightfo rward way.
The last co nstant we define is the UriMat che r. It's the utility that will tie mo st o f the o ther co nstants to gether.
To use a UriMatcher, yo u need to register each o f yo ur applicatio n's suppo rted URIs with the id co nstants.
Because the matcher is a static variable, we use a static blo ck to register each suppo rted URI. The matcher
do esn't take the full URI o bject directly; instead yo u register the URI autho rity and table name parts. So in the
static blo ck first line, sUriMat che r = ne w UriMat che r(UriMat che r.NO_MAT CH); we create the matcher that
gives a default match value o f NO_MAT CH. Next we call the addURI() metho d o n the matcher to register the
third parameter, o ur first id co nstant B_DAT A, with o ur co ntract's autho rity,
MyDat aCo nt ract s.AUT HORIT Y, and the table name, Dat aCo nt ract .NAME. Then we call addURI() again
to register the single ro w query id B_DAT A_ID. We use the same autho rity as we wo uld fo r all URIs we
register with the matcher. Fo r the seco nd parameter, we pass the table name again because it's the same
table. We also append the String " /# " to the table, which is used as a regular expressio n mask fo r all URIs
with this table name and an appended numeric id. Typically, this id is matched with a specific ro w id.
OBSERVE: MyDataCo ntentPro vider.java - Part 2
...
private MySQLHelper mDBHelper;
@Override
public boolean onCreate() {
mDBHelper = new MySQLHelper(getContext());
return true;
}
@Override
public String getType(Uri uri) {
int uriCode = sUriMatcher.match(uri);
switch (uriCode) {
case B_DATA:
return String.format(TYPE_DIR, DataContract.);
case B_DATA_ID:
return String.format(TYPE_ITEM, DataContract.NAME);
default:
throw new UnsupportedOperationException();
}
}
...
Next, we have the o nly member variable, a MySQLHe lpe r class o bject which we define belo w as an inner
class that extends the Andro id class SQLiteOpenHelper. The SQLiteOpenHelper class is used to interface
with a SQLite database. Yo u might remember this class fro m the first Andro id co urse.
The o nCre at e () metho d is used o nly to instantiate o ur SQLite helper. The ge t T ype () metho d uses the
matcher co nstant to match URIs to the MIME type co nstants we defined earlier. Since o ur MIME types will all
lo o k nearly the same, we use o ur St ring.f o rm at () ro utine to replace the % s part o f the appro priate MIME
type co nstant with the actual table name. This allo ws us to reuse the MIME type co nstants instead o f having to
type the full MIME type String fo r each table query type.
The matcher returns o ur URI co nstant int values, which allo ws us to use a swit ch/case blo ck to fo rmat and
return the pro per MIME type fo r the URI id:
OBSERVE: MyDataCo ntentPro vider.java - Part 3
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName)) {
long id = db.insert(tableName, null, values);
if (id > -1) {
ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(uri, null);
}
return ContentUris.withAppendedId(uri, id);
} else {
return null;
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] sel
ectionArgs, String sortOrder) {
SQLiteDatabase db = mDBHelper.getReadableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName)) {
Cursor cursor = db.query(tableName, projection, selection, selectionArgs
, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
} else {
return null;
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] sele
ctionArgs) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName))
return db.update(tableName, values, selection, selectionArgs);
else
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
String tableName = mDBHelper.getTableNameForCode(sUriMatcher.match(uri));
if (!TextUtils.isEmpty(tableName))
return db.delete(tableName, selection, selectionArgs);
else
return 0;
}
Co ntinuing with the member metho ds, we have the fo ur "CRUD" metho ds: inse rt (), que ry(), updat e () and
de le t e (). All fo ur o f these use the SQLit e Ope nHe lpe r to perfo rm its actio n o n the database. The que ry()
metho d gets a read-o nly versio n o f the database, while the o ther three metho ds use a read-write versio n o f
the database because they are actually making changes to the data. All fo ur use the helper metho d
ge t T able Nam e Fo rCo de (), that we added o n the DB helper class. ge t T able Nam e Fo rCo de () takes the
URI id fo und by the matcher and gives the appro priate table name. In additio n, each metho d perfo rms so me
erro r checking to make sure that we have a valid table name, then perfo rms its respective actio n.
After the inse rt metho d perfo rms the insert, a Co nt e nt Re so lve r o bject dispatches a no tificatio n that we've
changed the data, calling re so lve r.no t if yChange (uri, null);. The first parameter is the URI asso ciated with
the data we just inserted. The seco nd parameter is an o bject o f type Co nt e nt Obse rve r. We pass null fo r the
Co nt e nt Obse rve r to no tify all o bservers listening fo r changes o n this URI. If yo u o nly wanted to no tify a
single o bserver (and yo u have a reference to that o bserver) yo u wo uld pass the metho d that o bserver here.
The inse rt () metho d has a return type o f URI as well, which expects to have the lo ng id fro m the insert
appended to the o riginal URI. The Co nt e nt Uris helper class has a co nvenience metho d that allo ws yo u to
append o ur id to the o riginal URI.
In the que ry() metho d, after ge t t ing t he Curso r o bje ct f ro m o ur que ry, we register a no tificatio n URI
with the curso r with the line curso r.se t No t if icat io nUri(ge t Co nt e xt ().ge t Co nt e nt Re so lve r(), uri);.
This allo ws any future Curso rAdapt e rs (like the adapter in MainAct ivit y) to register a Co nt e nt Obse rve r
fo r the Curso r's URI and update the list with the new data auto matically.
The updat e () and de le t e () metho ds are nearly identical. They perfo rm the update o r the delete o n the table
and immediately return the result:
OBSERVE: MyDataCo ntentPro vider.java - Part 4
...
private class MySQLHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "MySqliteDB";
private static final int DB_VERSION = 1;
private static final String CREATE_TABLE_DATA = "CREATE TABLE " + DataContra
ct.NAME + " ("
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ DataContract.LABEL + " STRING, "
+ DataContract.VALUE + " INTEGER)";
public MySQLHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_DATA);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DataContract.NAME);
onCreate(db);
}
public String getTableNameForCode(int uriCode) {
switch (uriCode) {
case B_DATA:
case B_DATA_ID:
return DataContract.NAME;
default:
return null;
}
}
}
At the end o f o ur Co ntent Pro vider, we have o ur inner class MySQLHe lpe r definitio n. We de f ine t he cre at e
st at e m e nt s fo r each table in the database (just o ne, in o ur case). We take advantage o f o ur co nstants here
in the Co ntract to avo id typo s. We use the NAME co nstant with the "dro p table" call that we use in the
o nUpgrade () metho d. We have also added the helper metho d to find the appro priate table fo r a URI id value.
Again, we use a swit ch/case blo ck here to determine which table name to return fo r the uriCo de value.
No w that we've finished with the Co ntent Pro vider, let's lo o k at the MainActivity class, starting with the
o nCre at e () metho d:
OBSERVE: MainActivity.java o nCreate()
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDataLabelEdit = (EditText) findViewById(R.id.data_label_edit);
mDataValueEdit = (EditText) findViewById(R.id.data_value_edit);
mAddButton = (Button) findViewById(R.id.add_data_button);
mListView = (ListView) findViewById(android.R.id.list);
mProjection = new String[]{DataContract._ID, DataContract.LABEL, DataContrac
t.VALUE};
int[] viewIds = {R.id.id_label_text, R.id.data_label_text, R.id.value_label_
text};
mAdapter = new SimpleCursorAdapter(this, R.layout.item_data, null, mProjecti
on, viewIds, 0);
mListView.setAdapter(mAdapter);
LoaderManager lm = getSupportLoaderManager();
lm.initLoader(0, null, mLoaderCallbacks);
mAddButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addNewData();
}
});
mDataValueEdit.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_DONE) {
addNewData();
return true;
}
return false;
}
});
}
The first part o f this metho d is fairly standard. We set the layo ut and find all the views based o n their ids. Then
we do so me wo rk to prepare o ur Curso rAdapt e r. We're actually using an extensio n o f Curso rAdapt e r
called Sim ple Curso rAdapt e r, which will map data co lumn values to views auto matically, based o n the data
co lumn name and view id, respectively. First, we define o ur two arrays: an array o f e ach co lum n in o ur
t able t hat we are displaying (all o f t he m ), and an array o f the ids f o r t he vie ws in o ur
it e m _dat a.xm l layo ut where we want these values displayed. Finally, we cre at e o ur adapt e r, passing
the Co nt e xt t his, the list item layo ut R.layo ut .it e m _dat a, null fo r o ur Curso r since we do n't have o ne yet,
o ur two arrays m Pro je ct io n and vie wIds, and 0 fo r the flag parameter (since we do n't need to use the
adapter flags). Then, as with all list adapters, we call m List Vie w.se t Adapt e r(m Adapt e r); to set this
adapter o n the list view.
We are using a Curso rLo ade r to request o ur data, so we use the Lo ade rManage r to initialize o ur lo ader,
giving the init Lo ade r() m e t ho d o ur m Lo ade rCallbacks variable fo r the
Lo ade rManage r.Lo ade rCallbacks parameter. Finally, we register a callback metho d fo r the "Add" butto n:
OBSERVE: MainActivity.java addNewData()
private void addNewData() {
ContentValues values = new ContentValues();
String label = mDataLabelEdit.getText().toString();
String value = mDataValueEdit.getText().toString();
if (!TextUtils.isEmpty(label) && !TextUtils.isEmpty(value)) {
values.put(DataContract.LABEL, label);
values.put(DataContract.VALUE, value);
}
ContentResolver resolver = getContentResolver();
resolver.insert(DataContract.DATA_CONTENT_URI, values);
// Clear the old data
mDataLabelEdit.getText().clear();
mDataValueEdit.getText().clear();
mDataLabelEdit.requestFocus();
}
In the "Add" butto n callback metho d addNe wDat a(), we ge ne rat e a Co nt e nt Value s o bje ct that will be
supplied to the Co ntent Pro vider. A Co nt e nt Value s o bject maps table co lumn names with data values,
using a key/value pattern similar to a HashMap. We register the values fro m the two Edit T e xt views with o ur
Co nt e nt Value s o bject by calling value s.put (Dat aCo nt ract .LABEL, labe l) and
value s.put (Dat aCo nt ract .VALUE, value ). Then, instead o f getting a reference to o ur Co ntent Pro vider
directly, we use the Co nt e nt Re so lve r class, calling the inse rt () metho d with o ur table's URI
Dat aCo nt ract .DAT A_CONT ENT _URI, and o ur newly built Co nt e nt Value s. The Co ntentReso lver will find
a Co ntent Pro vider that is registered with the Andro id system to suppo rt this URI (which happens to be o ur
Co ntent Pro vider) and in turn, call the inse rt () metho d o n the Co ntent Pro vider with these same parameters.
After perfo rming the insert, we do a lit t le bit o f cle an up. We remo ve the text fro m the Edit T e xt views
calling ge t T e xt ().cle ar() o n each. Then we request fo cus back o n the first Edit T e xt view,
m Dat aLabe lEdit .re que st Fo cus(), to make it easier fo r the user to add new data to the list:
OBSERVE: MainActivity.java mLo aderCallbacks
private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderManag
er.LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(MainActivity.this, DataContract.DATA_CONTENT_URI
,
mProjection, null, null, DataContract.VALUE + " ASC");
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
};
In o ur Lo ade rCallbacks o nCre at e Lo ade r metho d, we cre at e a Curso rLo ade r t o re que st o ur dat a
f ro m t he Co nt e nt Pro vide r. The Curso r Lo ader will use the Co nt e nt Re so lve r just like we did in the
addNe wDat a() metho d; ho wever, using a Curso rLo ade r allo ws us to perfo rm this query asynchro no usly,
and also ensures that we can re-register o ur Curso r data with the Curso rAdapt e r efficiently after a
co nfiguratio n change such as a ro tatio n o f the device. A Curso rLo ade r co nstructo r takes parameters similar
to tho se that a call to Co nt e nt Pro vide r.que ry() metho d wo uld require: a co nt e xt (which is used by the
lo ader to find the Co nt e nt Re so lve r), the URI f o r t he que ry, a St ring array " pro je ct io n" o f which
co lum ns in t he t able we want t o re ce ive re sult s, a St ring se le ct io n, a St ring array o f se le ct io n
args, and a St ring de f ining t he so rt o rde r f o r t he dat a. Fo r the pro jectio n, we reuse the m Pro je ct io n
variable we defined when we built o ur adapter. We want all ro ws o f o ur table, so we pass null fo r the
se le ct io n and se le ct io n args. Fo r o ur so rt, we pass Dat aCo nt ract .VALUE + " ASC" to have the data
so rted o n the VALUE co lumn in ascending o rder.
If the o nLo ade rRe se t metho d has been called, we have no mo re data fo r the list, so we just pass null to
the adapter. Finally, in the o nLo adFinishe d() metho d, we supply the Curso rAdapt e r with the Curso r result
fro m the query:
OBSERVE: Andro idManifest.xml Pro vider tag
...
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
....
<provider
android:name="com.oreillyschool.android2.contentproviders.MyDataCont
entProvider"
android:authorities="com.oreillyschool.android2.contentproviders.pro
vider"
android:exported="false"
android:label="@string/app_name" / >
</application>
Finally, in o rder fo r the Andro id system to kno w abo ut o ur Co ntent Pro vider, we have to define it in the Andro id
Manifest. Just like Activities and Services, Pro vide r de f init io ns are nested inside o f the applicat io n tag.
The nam e param e t e r must be the full package and class name o f the pro vider. The aut ho rit y value must
match the AUTHORITY co nstant that we use in o ur Co ntract class to generate each co ntent URI used to
access the pro vider. The e xpo rt e d pro pe rt y is used to allo w o ther applicatio ns to access yo ur Co ntent
Pro vider. The labe l pro pe rt y sho uld be a user-readable string naming yo ur pro vider, so we re-use the
applicatio n name string reso urce.
Wrapping Up
Co ntent Pro viders can seem daunting at first. They tend to require a lo t o f setup co de just to get them wo rking.
Ho wever, after all the co nfiguratio n is in place, it's fairly straightfo rward to add suppo rt fo r new tables and queries. In
the end, they are a co nvenient way to abstract the data sto rage (and access lo gic) away fro m yo ur view lo gic. No w yo u
kno w ho w to define and register a new Co ntent Pro vider with the Andro id OS. Yo u've learned ho w to back a Co ntent
Pro vider with a SQLite database, and implement each o f the "CRUD" metho ds with the database. Yo u've also learned
ho w to access the Co ntent Pro vider using a Co ntent Reso lver and tie the resulting data to yo ur views with Curso rs.
These skills will help yo u create po werful Andro id applicatio ns with structured data. Nice wo rk! See yo u after yo u get
do ne with the ho mewo rk!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Camera Basics: Using the Built-in Camera Application
Lesson Objectives
In this lesso n yo u will:
create an Intent to launch the built-in camera applicatio n.
lo ad a Bitmap returned as a result fro m the built-in camera applicatio n.
save images fro m the built-in camera to external sto rage.
Starting the Built-in Camera Using an Intent
Like many Andro id's features, there are a co uple o f different ways to access an Andro id device's camera. The easiest
way is to hand o ff the camera utilizatio n to the device's default camera applicatio n. This do esn't allo w fo r much
custo mizatio n o f the pho to -taking pro cess, but it's relatively simple to implement. We'll co ver the specifics o f thi
pro cess in this lesso n.
Create a new Andro id pro ject with this criteria:
Name the pro ject Cam e raBasics.
Use the package name co m .o re illyscho o l.andro id2.cam e rabasics.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
No w update so me strings in st rings.xm l. Open it up and make these changes:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
name="app_name">Camera Basics</string>
name="action_settings">Settings</string>
name="hello_world">Hello World, MainActivity!</string>
name="capture_image">Snap it!</string>
</resources>
Open act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
</RelativeLinearLayout>
Next, o pen Andro idManif e st .xm l and make these changes:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.camerabasics"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.oreillyschool.android2.camerabasics.MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Finally, o pen MainAct ivit y.java and make these changes:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.camerabasics;
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.os.Bundle;
android.provider.MediaStore;
android.view.Menu;
android.view.View;
android.view.View.OnClickListener;
android.widget.ImageView;
public class MainActivity extends Activity {
private static final int TAKE_PICTURE_REQUEST_B = 100;
private ImageView mCameraImageView;
private Bitmap mCameraBitmap;
private OnClickListener mCaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
startImageCapture();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraImageView = (ImageView) findViewById(R.id.camera_image_view);
findViewById(R.id.capture_image_button).setOnClickListener(mCaptureImageButtonC
lickListener);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE_REQUEST_B) {
if (resultCode == RESULT_OK) {
// Recycle the previous bitmap.
if (mCameraBitmap != null) {
mCameraBitmap.recycle();
mCameraBitmap = null;
}
Bundle extras = data.getExtras();
mCameraBitmap = (Bitmap) extras.get("data");
mCameraImageView.setImageBitmap(mCameraBitmap);
} else {
mCameraBitmap = null;
}
}
}
private void startImageCapture() {
startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), TAKE_PICTUR
E_REQUEST_B);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Save all the mo dified files and run the applicatio n. When the applicatio n starts up, yo u see a mo stly blank screen with a
butto n at the bo tto m:
Note
Remember to use [Ct rl+F11] o r [Ct rl+F12] to ro tate the emulato r.
Click the Snap it ! butto n. The built-in camera applicatio n will start up:
There's no actual physical camera when yo u use the emulato r; the preview area o f the camera applicatio n is
co mpletely white. On an actual device, yo u'd see a live preview.
Go ahead and click the camera shutter butto n to take a picture. Yo u hear a shutter click so und, and the interface
changes to display Cance l, Re t ake , and OK butto ns. Click OK. No w yo u see the main screen again with a
placeho lder picture. This emulato r returns the placeho lder as the taken "picture" (even tho ugh the built-in applicatio n
sho wed o nly white space).
It may be a difficult to identify because the emulato r uses white space and a placeho lder, but the applicatio n
successfully called the built-in camera applicatio n and received the picture taken. Let's take a lo o k at ho w that
happened:
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
</LinearLayout>
In o ur layo ut, we add a <But t o n> to launch the built-in camera applicatio n and an Im age Vie w to ho ld the image
returned by that applicatio n:
OBSERVE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.camerabasics"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
In the manifest, we add andro id:scre e nOrie nt at io n=" landscape " to the <applicatio n> no de's attributes to fo rce
o ur applicatio n to be landscape-o riented in o rder to match the default o rientatio n o f typical camera applicatio ns:
OBSERVE: MainActivity.java
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraImageView = (ImageView) findViewById(R.id.camera_image_view);
findViewById(R.id.capture_image_button).setOnClickListener(mCaptureImageButtonClick
Listener);
}
The bulk o f the wo rk is do ne in MainActivity.java. In the o nCre at e metho d, we se t up t he UI and grab a re f e re nce
t o t he Im age Vie w t o ho ld t he cam e ra im age . We also wire up a Vie w.OnClickList e ne r instance to o ur Snap
it ! butto n. When the user clicks this butto n, the listener calls the st art Im age Capt ure metho d. The
st art Im age Capt ure metho d starts the built-in camera applicatio n by calling Act ivit y.st art Act ivit yFo rRe sult and
passing it a new Int e nt . The Int e nt 's actio n is Me diaSt o re .ACT ION_IMAGE_CAPT URE, which specifically
instructs the Int e nt to use the device's default applicatio n to capture images (that is, the default Camera app). We also
pass a custo m request co de that we defined earlier to st art Act ivit yFo rRe sult so that we can handle the image
returned by the camera applicatio n in o nAct ivit yRe sult .
OBSERVE:
...
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE_REQUEST_B) {
if (resultCode == RESULT_OK) {
// Recycle the previous bitmap.
if (mCameraBitmap != null) {
mCameraBitmap.recycle();
mCameraBitmap = null;
}
Bundle extras = data.getExtras();
mCameraBitmap = (Bitmap) extras.get("data");
mCameraImageView.setImageBitmap(mCameraBitmap);
} else {
mCameraBitmap = null;
}
}
}
...
In o nAct ivit yRe sult , first we che ck t o se e if we have an e xist ing im age f ro m t he cam e ra, and if so , we
re cycle t hat im age , calling m Cam e raBit m ap.re cycle (). It is impo rtant to recycle bitmaps in Andro id co rrectly
when yo u're finished using them. This frees up the applicatio n's reserved heap space fo r images, which can be limited.
Next we pull o ut t he dat a se nt back f ro m t he cam e ra applicat io n and place t his Bit m ap int o t he
Im age Vie w o f o ur applicat io n. The Bitmap returned by the built-in camera applicatio n is sto red inside the Int e nt
passed to o nAct ivit yRe sult , in its e xt ras bundle, under the key " dat a" .
The Andro id Develo per Do cumentatio n o n image capture intents lists an extra Uri that may be sent with the Int e nt
under the key Me diaSt o re .EXT RA_OUT PUT . The Uri is an o ptio nal parameter that allo ws yo u to specify a path and
filename fo r the captured image. In general, yo u can do this to save the the image data to a file. We didn't take
advantage o f that capability here tho ugh because the emulato r do esn't actually send the extra. In fact, if we were to use
it when the camera applicatio n do esn't suppo rt it, the Int e nt returned in o nAct ivit yRe sult wo uld be null. The
do cumentio n stro ngly suggests that yo u use this extra. While it do es no t wo rk well in the emulato r, when yo u use an
Int e nt to o pen the built-in camera applicatio n, it's go o d practice to utilize the Me diaSt o re .EXT RA_OUT PUT and
test o n an actual device.
Saving Image to External Storage
While saving an image to external sto rage is no t tied specifically into using a camera applicatio n (built-in o r custo m),
it's a co mmo n task when wo rking with cameras and images, so let's add this feature to o ur applicatio n.
First, make these changes to st rings.xm l:
CODE TO TYPE: strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
name="app_name">Camera Basics</string>
name="action_settings">Settings</string>
name="capture_image">Snap it!</string>
name="save_image">Save Picture</string>
</resources>
Make these changes to act ivit y_m ain.xm l:
CODE TO TYPE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center" >
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
<Button
android:id="@+id/save_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/save_image"/>
</LinearLayout>
</LinearLayout>
No w make these changes to Andro idManif e st .xm l:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.camerabasics"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Finally, make these changes to MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.camerabasics;
import
import
import
import
import
java.io.File;
java.io.FileOutputStream;
java.text.SimpleDateFormat;
java.util.Date;
java.util.Locale;
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.os.Bundle;
android.os.Environment;
android.provider.MediaStore;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.ImageView;
android.widget.Toast;
public class MainActivity extends Activity {
private static final int TAKE_PICTURE_REQUEST_B = 100;
private ImageView mCameraImageView;
private Bitmap mCameraBitmap;
private Button mSaveImageButton;
private OnClickListener mCaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
startImageCapture();
}
};
private OnClickListener mSaveImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
File saveFile = openFileForImage();
if (saveFile != null) {
saveImageToFile(saveFile);
} else {
Toast.makeText(MainActivity.this, "Unable to open file for saving image.",
Toast.LENGTH_LONG).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraImageView = (ImageView) findViewById(R.id.camera_image_view);
findViewById(R.id.capture_image_button);.setOnClickListener(mCaptureImageButtonClic
kListener);
mSaveImageButton = (Button) findViewById(R.id.save_image_button);
mSaveImageButton.setOnClickListener(mSaveImageButtonClickListener);
mSaveImageButton.setEnabled(false);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE_REQUEST_B) {
if (resultCode == RESULT_OK) {
// Recycle the previous bitmap.
if (mCameraBitmap != null) {
mCameraBitmap.recycle();
mCameraBitmap = null;
}
Bundle extras = data.getExtras();
mCameraBitmap = (Bitmap) extras.get("data");
mCameraImageView.setImageBitmap(mCameraBitmap);
mSaveImageButton.setEnabled(true);
} else {
mCameraBitmap = null;
mSaveImageButton.setEnabled(false);
}
}
}
private void startImageCapture() {
startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), TAKE_PICTURE_RE
QUEST_B);
}
private File openFileForImage() {
File imageDirectory = null;
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
imageDirectory = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"com.oreillyschool.android2.camerabasics");
if (!imageDirectory.exists() && !imageDirectory.mkdirs()) {
imageDirectory = null;
} else {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_mm_dd_hh_mm",
Locale.getDefault());
return new File(imageDirectory.getPath() +
File.separator + "image_" + dateFormat.format(new Date()) + ".png");
}
}
return null;
}
private void saveImageToFile(File file) {
if (mCameraBitmap != null) {
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(file);
if (!mCameraBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "Saved image to: " + file.getPath(), Toast.
LENGTH_LONG).show();
}
outStream.close();
} catch (Exception e) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
}
}
}
}
Save the mo dified files and run the applicatio n. No w we have two butto ns instead o f o ne: Snap it ! and Save Im age :
The Save Im age butto n is disabled because we haven't taken a picture yet. Click Snap it !, use the built-in camera
applicatio n, take a picture, and click OK to return to the applicatio n. Yo u see the same Andro id placeho lder image as
befo re, and the Save Im age butto n is no w enabled. Click o n it, and yo u'll see a to ast message that indicates that the
file has been saved. The message also includes the name o f the file saved:
Note
The next step changes the perspective in the sandbo x. To return to the sandbo x and this lesso n co ntent
later, select Windo w | Clo se Pe rspe ct ive .
The images are saved by go ing to the DDMS perspective: select Windo w | Ope n Pe rspe ct ive | Ot he r... | DDMS, o r
clickin the do uble arro w at the to p right and selecting DDMS Pe rspe ct ive :
Select the emulato r in the De vice s tab, go to the File Explo re r, and then go to
m nt /sdcard/Pict ure s/co m .o re illyscho o l.andro id2.cam e rabasics; all o f the files we saved:
Let's review ho w we were able to save the images to the emulato r's SD card:
OBSERVE: Andro idManifest.xml
...
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
First, we added the <use s-pe rm issio n /> tag to o ur manifest. This declares to the Andro id OS that o ur applicatio n
requires permissio n to write data to the device SD card. If this applicatio n is published to the Go o gle Play market,
users will see this permissio n listed as a requirement. By do wnlo ading the applicatio n, they presumably "grant" that
permissio n to the applicatio n:
OBSERVE: activity_main.xml
...
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center" >
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
<Button
android:id="@+id/save_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/save_image"/>
</LinearLayout>
</LinearLayout>
Next, we added a but t o n to the UI so that the user can elect to save the current displayed image to a file:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
private static final int TAKE_PICTURE_REQUEST_B = 100;
private ImageView mCameraImageView;
private Bitmap mCameraBitmap;
private Button mSaveImageButton;
private OnClickListener mCaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
startImageCapture();
}
};
private OnClickListener mSaveImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
File saveFile = openFileForImage();
if (saveFile != null) {
saveImageToFile(saveFile);
} else {
Toast.makeText(MainActivity.this, "Unable to open file for saving image.",
Toast.LENGTH_LONG).show();
}
}
};
...
The bulk o f o ur changes were to the MainActivity.java file. At the to p, we added a Vie w.OnClickList e ne r f o r t he
Save Im age but t o n. This listener first at t e m pt s t o o pe n a f ile t o save t he im age , and if t he f ile is o pe ne d
succe ssf ully, the listener writ e s t he im age dat a t o t he f ile . If it co uld no t o pe n a f ile , it displays a t o ast
m e ssage indicat ing t hat .
To o pen a file and save the image to it, the listener calls two new metho ds: o pe nFile Fo rIm age and
save Im age T o File :
OBSERVE: MainActivity.java
...
private File openFileForImage() {
File imageDirectory = null;
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
imageDirectory = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"com.oreillyschool.android2.camerabasics");
if (!imageDirectory.exists() && !imageDirectory.mkdirs()) {
imageDirectory = null;
} else {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_mm_dd_hh_mm",
Locale.getDefault());
return new File(imageDirectory.getPath() +
File.separator + "image_" + dateFormat.format(new Date()) + ".png");
}
}
return null;
}
...
The first metho d, o pe nFile Fo rIm age , tries to o pen a file to save the image o n the SD card. Specifically, it t rie s t o
cre at e t he f ile in a subf o lde r o f t he " Pict ure s" f o lde r o f t he SD card. To do this, it checks to determine
whether the sto rage is actually mo unted by calling Enviro nm e nt .ge t Ext e rnalSt o rage St at e (). If the media is
mo unted, then it tries to o pen the " Pict ure s" dire ct o ry o f t he SD card. If " Pict ure s" do e s no t e xist , then it tries
to create the directo ry. Finally, o nce a reference to the directo ry is o btained, o pe nFile Fo rIm age writ e s t he im age
dat a t o a PNG f ile . If any o f these steps fails in o pening the directo ry o r the file, o pe nFile Fo rIm age re t urns null.
Note
Enviro nm e nt co ntains several co nstants that represent the vario us po tential states o f external sto rage.
Enviro nm e nt also co ntains several co nstants fo r standard directo ry names fo r Andro id, such as the
"Pictures" fo lder that we used in o ur applicatio n. There are also static metho ds o n Enviro nm e nt that
allo w yo u to request info rmatio n o n an Andro id device's file system. Fo r mo re info rmatio n, see the
Andro id Develo per do cumentatio n:
OBSERVE: MainActivity.java
...
private void saveImageToFile(File file) {
if (mCameraBitmap != null) {
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(file);
if (!mCameraBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "Saved image to: " + file.getPath(), Toast.
LENGTH_LONG).show();
}
outStream.close();
} catch (Exception e) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
}
}
}
...
The seco nd metho d, save Im age T o File writ e s t he im age dat a t o t he o pe ne d f ile . If there are any erro rs in the
file, then a t o ast m e ssage is displaye d de scribing t he issue . If the file is written succesfully, a t o ast m e ssage
wit h t he f ile 's nam e is displaye d.
Wrapping Up
In this lesso n, we made an applicatio n that allo ws users to take pho to s and then save tho se pho to s to the device's SD
Card. Our applicatio n also used the device's default Camera applicatio n to take the pho to s. Accessing the built-in
camera applicatio n with an Int e nt is the mo st straightfo rward way to wo rk with a device's camera. This metho d will
pro bably meet mo st o f yo ur image-capturing needs. Usually the built-in camera applicatio n is equipped with a full
range o f features including auto fo cus, flash, and scenes (actio n, po rtrait, macro ,and so o n). It's co nvenient to retrieve
the image data o nce the camera applicatio n is do ne.
If yo u want to create a truly custo m camera applicatio n, yo u can dive into the Andro id Camera API that we'll co ver in the
next lesso n. See yo u there!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Camera Advanced: Building a Custom Camera
Application
Lesson Objectives
In this lesso n yo u will:
o pen the hardware camera.
create a live preview o f the camera image.
take images with the camera.
release the camera reso urces.
tag camera features that yo ur applicatio n uses fo r Go o gle Play.
find advanced camera functio nality.
In the previo us lesso n, we saw ho w to use the built-in camera applicatio n o n mo st devices: we started the applicatio n via an
Int e nt , received the picture taken in Act ivit y.o nAct ivit yRe sult , and saved the picture to external sto rage. If yo ur applicatio n
o nly needs camera functio nality o nce in a while, yo u'll pro bably be able to get by with the built-in camera applicatio n. Ho wever, if
yo ur applicatio n revo lves aro und pho to graphy, it may need custo m functio nality that the sto ck camera do esn't suppo rt.
When yo u need to create a custo m camera, yo u can use Andro id's Cam e ra o bject. The Cam e ra o bject has many o ptio ns and
features, but this makes it much mo re co mplex to use than the built-in camera applicatio n.
Also , since we test o ur applicatio ns o nly in the emulato r, the features we can use will be limited. In this lesso n we'll review
where yo u can find these features if yo u need to create a custo m camera in the future.
The co de fo r this pro ject will be similar to the pro ject fro m the Camera Basics lesso n. We'll create a new pro ject to keep the
functio nality separate, but we'll reuse much o f the co de fro m Andro idManif e st .xm l, MainAct ivit y.java, and
act ivit y_m ain.xm l. Create a new Andro id pro ject with this criteria.
Name the pro ject Cam e raAdvance d.
Use the package name co m .o re illyscho o l.andro id2.cam e raadvance d.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
Using the Camera API
We'll start o ff by creating a new Activity that will take o ver the wo rk do ne by the built-in camera applicatio n. Fo r this
we're go ing to need a new Activity class, a view, and so me related manifest updates. First, tho ugh, let's update the
strings. Open st rings.xm l and make these changes:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
<string
<string
<string
<string
<string
name="app_name">Camera Advanced</string>
name="action_settings">Settings</string>
name="hello_world">Hello World, MainActivity!</string>
name="start_image_capture">Take a New Picture</string>
name="capture_image">Snap it!</string>
name="save_image">Save Picture</string>
name="recapture_image">Retake Picture</string>
name="capturing_image">Taking New Picture</string>
name="done">Done</string>
</resources>
Next, o pen act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center" >
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/start_image_capture" />
<Button
android:id="@+id/save_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/save_image" />
</LinearLayout>
</LinearLayout>
Next, create a new Andro id XML file, set its type to Layo ut , name it act ivit y_cam e ra, ensure that its ro o t element is
Line arLayo ut , and make these changes:
CODE TO TYPE:activity_camera.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#999999"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/capturing_image" />
<FrameLayout
android:id="@+id/camera_frame"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" >
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<SurfaceView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
<Button
android:id="@+id/done_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/done" />
</LinearLayout>
</LinearLayout>
Next, create a new class named Cam e raAct ivit y that extends andro id.app.Act ivit y and make these changes:
CODE TO TYPE: CameraActivity.java
package com.oreillyschool.android2.cameraadvanced;
import java.io.IOException;
import
import
import
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.hardware.Camera;
android.hardware.Camera.PictureCallback;
android.os.Bundle;
android.view.SurfaceHolder;
android.view.SurfaceView;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.ImageView;
android.widget.Toast;
public class CameraActivity extends Activity implements PictureCallback, SurfaceHolder.
Callback {
public static final String EXTRA_CAMERA_DATA = "camera_data";
private static final String KEY_IS_CAPTURING = "is_capturing";
private
private
private
private
private
private
Camera mCamera;
ImageView mCameraImage;
SurfaceView mCameraPreview;
Button mCaptureImageButton;
byte[] mCameraData;
boolean mIsCapturing;
private OnClickListener mCaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
captureImage();
}
};
private OnClickListener mRecaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
setupImageCapture();
}
};
private OnClickListener mDoneButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mCameraData != null) {
Intent intent = new Intent();
intent.putExtra(EXTRA_CAMERA_DATA, mCameraData);
setResult(RESULT_OK, intent);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
mCameraImage = (ImageView) findViewById(R.id.camera_image_view);
mCameraImage.setVisibility(View.INVISIBLE);
mCameraPreview = (SurfaceView) findViewById(R.id.preview_view);
final SurfaceHolder surfaceHolder = mCameraPreview.getHolder();
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mCaptureImageButton = (Button) findViewById(R.id.capture_image_button);
mCaptureImageButton.setOnClickListener(mCaptureImageButtonClickListener);
final Button doneButton = (Button) findViewById(R.id.done_button);
doneButton.setOnClickListener(mDoneButtonClickListener);
mIsCapturing = true;
}
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean(KEY_IS_CAPTURING, mIsCapturing);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mIsCapturing = savedInstanceState.getBoolean(KEY_IS_CAPTURING, mCameraData == null)
;
if (mCameraData != null) {
setupImageDisplay();
} else {
setupImageCapture();
}
}
@Override
protected void onResume() {
super.onResume();
if (mCamera == null) {
try {
mCamera = Camera.open();
mCamera.setPreviewDisplay(mCameraPreview.getHolder());
if (mIsCapturing) {
mCamera.startPreview();
}
} catch (Exception e) {
Toast.makeText(CameraActivity.this, "Unable to open camera.", Toast.LENGTH_LONG
)
.show();
}
}
}
@Override
protected void onPause() {
super.onPause();
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mCameraData = data;
setupImageDisplay();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mCamera != null) {
try {
mCamera.setPreviewDisplay(holder);
if (mIsCapturing) {
mCamera.startPreview();
}
} catch (IOException e) {
Toast.makeText(CameraActivity.this, "Unable to start camera preview.", Toast.LE
NGTH_LONG).show();
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
private void captureImage() {
mCamera.takePicture(null, null, this);
}
private void setupImageCapture() {
mCameraImage.setVisibility(View.INVISIBLE);
mCameraPreview.setVisibility(View.VISIBLE);
mCamera.startPreview();
mCaptureImageButton.setText(R.string.capture_image);
mCaptureImageButton.setOnClickListener(mCaptureImageButtonClickListener);
}
private void setupImageDisplay() {
Bitmap bitmap = BitmapFactory.decodeByteArray(mCameraData, 0, mCameraData.length);
mCameraImage.setImageBitmap(bitmap);
mCamera.stopPreview();
mCameraPreview.setVisibility(View.INVISIBLE);
mCameraImage.setVisibility(View.VISIBLE);
mCaptureImageButton.setText(R.string.recapture_image);
mCaptureImageButton.setOnClickListener(mRecaptureImageButtonClickListener);
}
}
Next, o pen Andro idManif e st .xm l and make these changes:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.cameraadvanced"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".CameraActivity"
android:label="@string/capture_image"
android:screenOrientation="landscape" />
</application>
</manifest>
Finally, make the changes belo w to MainAct ivit y.java. It will be the same as the previo us lesso n's MainAct ivit y
class, except fo r the lines that are highlighted as having been changed. If yo u're writing this class fro m scratch, be sure
to add all this co de:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.cameraadvanced;
import
import
import
import
import
java.io.File;
java.io.FileOutputStream;
java.text.SimpleDateFormat;
java.util.Date;
java.util.Locale;
import
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.os.Bundle;
android.os.Environment;
android.provider.MediaStore;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.ImageView;
android.widget.Toast;
public class MainActivity extends Activity {
private static final int TAKE_PICTURE_REQUEST_B = 100;
private ImageView mCameraImageView;
private Bitmap mCameraBitmap;
private Button mSaveImageButton;
private OnClickListener mCaptureImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
startImageCapture();
}
};
private OnClickListener mSaveImageButtonClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
File saveFile = openFileForImage();
if (saveFile != null) {
saveImageToFile(saveFile);
} else {
Toast.makeText(MainActivity.this, "Unable to open file for saving image.",
Toast.LENGTH_LONG).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraImageView = (ImageView) findViewById(R.id.camera_image_view);
findViewById(R.id.capture_image_button).setOnClickListener(mCaptureImageButtonClick
Listener);
mSaveImageButton = (Button) findViewById(R.id.save_image_button);
mSaveImageButton.setOnClickListener(mSaveImageButtonClickListener);
mSaveImageButton.setEnabled(false);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE_REQUEST_B) {
if (resultCode == RESULT_OK) {
// Recycle the previous bitmap.
if (mCameraBitmap != null) {
mCameraBitmap.recycle();
mCameraBitmap = null;
}
Bundle extras = data.getExtras();
mCameraBitmap = (Bitmap) extras.get("data");
byte[] cameraData = extras.getByteArray(CameraActivity.EXTRA_CAMERA_DATA);
if (cameraData != null) {
mCameraBitmap = BitmapFactory.decodeByteArray(cameraData, 0, cameraData.lengt
h);
mCameraImageView.setImageBitmap(mCameraBitmap);
mSaveImageButton.setEnabled(true);
}
} else {
mCameraBitmap = null;
mSaveImageButton.setEnabled(false);
}
}
}
private void startImageCapture() {
startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), TAKE_PICTURE_RE
QUEST_B);
startActivityForResult(new Intent(MainActivity.this, CameraActivity.class), TAKE_PI
CTURE_REQUEST_B);
}
private File openFileForImage() {
File imageDirectory = null;
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
imageDirectory = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"com.oreillyschool.android2.camera");
if (!imageDirectory.exists() && !imageDirectory.mkdirs()) {
imageDirectory = null;
} else {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_mm_dd_hh_mm",
Locale.getDefault());
return new File(imageDirectory.getPath() +
File.separator + "image_" +
dateFormat.format(new Date()) + ".png");
}
}
return null;
}
private void saveImageToFile(File file) {
if (mCameraBitmap != null) {
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(file);
if (!mCameraBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "Saved image to: " + file.getPath(),
Toast.LENGTH_LONG).show();
}
outStream.close();
} catch (Exception e) {
Toast.makeText(MainActivity.this, "Unable to save image to file.",
Toast.LENGTH_LONG).show();
}
}
}
}
No w save all o f the mo dified files and run the applicatio n. On startup, the applicatio n lo o ks just abo ut the same as
befo re, tho ugh we did change the label text a little bit:
Click the T ake a Ne w Pict ure butto n to launch o ur new, custo m camera activity:
If o ur applicatio n were running o n an actual device with a camera, we wo uld be able to see a camera preview in the
white space. Click the Snap it ! butto n, and yo u see an emulated image—the same Andro id placeho lder that we saw
befo re:
Click the Do ne butto n in the camera activity to return to the main activity and see the Save butto n, like we did befo re
when we used the built-in camera applicatio n:
Okay, that was a lo t o f co de. Let's take a lo o k at ho w we created o ur custo m camera. The changes made to o ur main
activity UI are relativly mino r: changing so me butto n text. The bulk o f the new co de was fo r the camera activity:
OBSERVE: activity_camera.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#999999"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/capturing_image" />
<FrameLayout
android:id="@+id/camera_frame"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" >
<ImageView
android:id="@+id/camera_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<SurfaceView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/capture_image_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/capture_image" />
<Button
android:id="@+id/done_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/done" />
</LinearLayout>
</LinearLayout>
The layo ut fo r the CameraActivity co nsists o f two main areas: a Fram e Layo ut that ho lds a Surf ace Vie w and an
Im age Vie w, and a Line arLayo ut that co ntains but t o ns f o r use r act io ns.
The Andro id Cam e ra o bject utilizes the Surf ace Vie w we added to preview live images fro m the camera (if yo u aren't
using the emulato r). A Surf ace Vie w is a type o f Vie w in Andro id reserved fo r drawing.
The Im age Vie w will ho ld the capture image.
The first butto n allo ws the user to snap the pho to . The seco nd butto n allo ws the user to return to the main activity with
the snapped image:
OBSERVE: CameraActivity.java
...
public class CameraActivity extends Activity implements PictureCallback, SurfaceHolder.
Callback {
public static final String EXTRA_CAMERA_DATA = "camera_data";
private static final String KEY_IS_CAPTURING = "is_capturing";
private
private
private
private
private
private
Camera mCamera;
ImageView mCameraImage;
SurfaceView mCameraPreview;
Button mCaptureImageButton;
byte[] mCameraData;
boolean mIsCapturing;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
mCameraImage = (ImageView) findViewById(R.id.camera_image_view);
mCameraImage.setVisibility(View.INVISIBLE);
mCameraPreview = (SurfaceView) findViewById(R.id.preview_view);
final SurfaceHolder surfaceHolder = mCameraPreview.getHolder();
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mCaptureImageButton = (Button) findViewById(R.id.capture_image_button);
mCaptureImageButton.setOnClickListener(mCaptureImageButtonClickListener);
final Button doneButton = (Button) findViewById(R.id.done_button);
doneButton.setOnClickListener(mDoneButtonClickListener);
mIsCapturing = true;
}
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean(KEY_IS_CAPTURING, mIsCapturing);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mIsCapturing = savedInstanceState.getBoolean(KEY_IS_CAPTURING, mCameraData == null)
;
if (mCameraData != null) {
setupImageDisplay();
} else {
setupImageCapture();
}
}
@Override
protected void onResume() {
super.onResume();
if (mCamera == null) {
try {
mCamera = Camera.open();
mCamera.setPreviewDisplay(mCameraPreview.getHolder());
if (mIsCapturing) {
mCamera.startPreview();
}
} catch (Exception e) {
Toast.makeText(CameraActivity.this, "Unable to open camera.", Toast.LENGTH_LONG
)
.show();
}
}
}
@Override
protected void onPause() {
super.onPause();
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mCameraData = data;
setupImageDisplay();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mCamera != null) {
try {
mCamera.setPreviewDisplay(holder);
if (mIsCapturing) {
mCamera.startPreview();
}
} catch (IOException e) {
Toast.makeText(CameraActivity.this, "Unable to start camera preview.", Toast.LE
NGTH_LONG).show();
}
}
}
...
}
The Cam e raAct ivit y grabs co ntro l o f the camera and do es the wo rk o f setting up the live preview and taking pictures,
as well as passing back the image data to the activity that started it.
The central class o f the camera API is the Cam e ra class. The Cam e ra pro vides info rmatio n abo ut the device camera,
access to camera settings, and co ntro l o ver picture preview and picture taking. To get co ntro l o f the device camera, we
cre at e a Cam e ra inst ance and call Cam e ra.o pe n(). If the camera is available, the applicatio n will have co ntro l o f
the camera. If any o ther applicatio ns try to o pen the camera, the call will thro w a Runt im e Exce pt io n. To release
co ntro l o f the camera so that ano ther applicatio n can use it, we call Cam e ra.re le ase ().
It is vital to release camera reso urces if yo ur applicatio n is no t actively using them, including when yo ur applicatio n is
paused in the backgro und. In fact, if yo u do no t release the reso urces and try to o pen the camera again, yo u'll get a
Runt im e Exce pt io n even if yo u already had co ntro l. To avo id that, we place o ur Cam e ra.o pe n() call in
o nRe sum e () and o ur Cam e ra.re le ase () call in o nPause ().
The Cam e ra.o pe n() call returns a Cam e ra o bject that we sto re in a member variable. We will use this instance fo r all
o f o ur o ther camera functio nality.
Ano ther piece o f functio nality that we start in o nRe sum e is the live preview. Befo re we go o ver ho w to start the
preview, we need to discuss the Surf ace Vie w where the preview is actually drawn. The camera can use the
Surf ace Vie w to draw the live preview o nly after the surface has been created and sized. If we try to start the camera
preview befo re the Surf ace Vie w's surface fully initializes, we wo n't get any exceptio ns, but the live preview will no t
render. In o rder to start the preview after the surface is initialized, we have Cam e raAct ivit y implement the
Surf ace Ho lde r.Callback interface. Surf ace Ho lde r is an interface fo r manipulating the surface o f a Surf ace Vie w
and is in fact what we pass to the Cam e ra fo r the preview. By having Cam e ra implement the callback interface, we can
have o ur Cam e ra instance start the preview after the surface initializes in
Surf ace Ho lde r.Callback.surf ace Change d.. So in o nCre at e , we get a reference to the Surf ace Vie w, and then
use it s Surf ace Ho lde r t o add t he Cam e raAct ivit y as a callback and also se t t he surf ace 's t ype .
Note
The valid co nstant values fo r Surf ace Ho lde r.se t T ype are
Surf ace Ho lde r.SURFACE_T YPE_NORMAL and
Surf ace Ho lde r.SURFACE_T YPE_PUSH_BUFFERS. Ho wever, this metho d and these co nstants are
deprecated as o f API 11. So , in yo ur pro jects, if yo ur minimum build target is an API higher than 10 , yo u do
no t need to call this metho d because the type is set auto matically when needed. Fo r mo re info rmatio n,
see the Andro id Develo per Do cumentatio n.
The Surf ace Ho lde r.Callback co nsists o f three metho ds: surf ace Cre at e d, surf ace Change d, and
suf ace De st ro ye d. The o nly o ne we need fo r o ur purpo ses is surf ace Change d. In o ur implementatio n o f
surf ace Change d, we check to determine whether Cam e raAct ivit y has a valid Cam e ra instance; if it do es we
assign the Surf ace Ho lde r to the Cam e ra as its display surface. At the to p o f the class, we add a member variable,
m IsCapt uring as a flag to differentiate when we are previewing the camera and when we are displaying a taken
picture. So in surf ace Change d, we check this flag; if we are currently trying to capture an image, we go ahead and call
Cam e ra.st art Pre vie w() to start the preview drawing. These metho d calls are all wrapped in a try-catch blo ck so that if
there are any issues starting the preview, we can display a to ast message to the user indicating the pro blem.
To take a picture, o ur capt ure Im age () metho d simply calls the Cam e ra.t ake Pict ure metho d. There are two
versio ns o f Cam e ra.t ake Pict ure :
public final vo id takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw,
Camera.PictureCallback jpeg)
public final vo id takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw,
Camera.PictureCallback po stview, Camera.PictureCallback jpeg)
The first metho d calls the seco nd metho d with all its parameters, but passes null fo r the Cam e ra.Pict ure Callback
po st vie w argument. Let's analyze the seco nd metho d. The first argument is a Cam e ra.Shut t e rCallback. This
callback is called the mo ment the image is captured. The seco nd argument is a Cam e ra.Pict ure Callback. It is called
when raw image data is available. The third argument is a Cam e ra.Pict ure Callback. It is called when the scaled,
pro cessed "po stview" image is available. The last argument fo r bo th is a Cam e ra.Pict ure Callback. It is called when
JPEG image data is called. It's o kay to pass null fo r any o f these arguments if yo u do n't care abo ut that particular
callback.
In o ur applicatio n, we use the first versio n and o nly set a callback fo r JPEG image data. We set the o thers to null
because we do n't need them. We have the Cam e raAct ivit y implement Pict ure Callback, so when the picture is
taken and the JPEG data is available, o nPict ure T ake n is called. The callback is passed a byte array co ntaining the
picture data, then we save the data to a member variable and call the se t upIm age Display() metho d to call
Cam e ra.st o pPre vie w(), hide the Surf ace Vie w, and deco de the byte array into a Bit m ap, which is then displayed in
the Im age Vie w. Also , we change the "Snap it!" butto n text to "Retake." When clicked, instead o f calling
Cam e ra.t ake Pict ure , the "Retake" butto n, will call se t upIm age Capt ure , which hides the Im age Vie w, sho ws the
Surf ace Vie w, calls Cam e ra.st art Pre vie w() to start the live preview again, and changes the "Retake" butto n text
back to "Snap it!"
The final piece is to return the image data to o ur MainAct ivit y. We acco mplish this with the "Do ne" butto n. In the click
listener fo r this butto n, we take the byte array received in o nPict ure T ake n and place that as an extra in a new Int e nt
instance. Then we set this Int e nt as the result fo r this activity via Act ivit y.se t Re sult and call Act ivit y.f inish() to
return to end the Cam e raAct ivit y, and return to the MainAct ivit y.
OBSERVE: MainActivity.java
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE_REQUEST_B) {
if (resultCode == RESULT_OK) {
// Recycle the previous bitmap.
if (mCameraBitmap != null) {
mCameraBitmap.recycle();
mCameraBitmap = null;
}
Bundle extras = data.getExtras();
byte[] cameraData = extras.getByteArray(CameraActivity.EXTRA_CAMERA_DATA);
if (cameraData != null) {
mCameraBitmap = BitmapFactory.decodeByteArray(cameraData, 0, cameraData.lengt
h);
mCameraImageView.setImageBitmap(mCameraBitmap);
mSaveImageButton.setEnabled(true);
}
} else {
mCameraBitmap = null;
mSaveImageButton.setEnabled(false);
}
}
}
private void startImageCapture() {
startActivityForResult(new Intent(MainActivity.this, CameraActivity.class), TAKE_PI
CTURE_REQUEST_B);
}
...
Back in MainAct ivit y we made a co uple o f mino r changes to o ur previo us applicatio n. We can still retrieve the data
fro m Cam e raAct ivit y in o nAct ivit yRe sult like we did with the built-in camera. Ho wever, here t he ke y f o r t he
value st o re d in t he Int e nt is different, because we made o ur o wn co nstant. Also , since we were passing back a
byte array, instead o f assigning a Bit m ap to the MainAct ivit y's Im age Vie w, MainAct ivit y.o nAct ivit yRe sult
re t rie ve s t he byt e array, de co de s it int o a Bit m ap, and then assigns it t o t he Im age Vie w.
Camera Parameters
Mo st cameras o n newer devices have many settings that yo u can access thro ugh the Camera API. Unfo rtunately, they
are hard to test o n an emulato r. Regardless, let's still review ho w yo u wo uld access these camera parameters, the
kind o f settings yo u can manipulate with the Cam e ra.Param e t e rs class, and adjust camera settings.
The current settings fo r a Cam e ra instance are o btained by calling Cam e ra.ge t Param e t e rs(). There are several
settings yo u can change by calling setters o n the Cam e ra.Param e t e rs instance returned. Fo r example, yo u can set
anti-banding, co lo ring effects (sepia, negative, and so o n), flash mo de, fo cus mo de, scene mo de, white balance,
preview size, and JPEG quality, amo ng o ther things. Fo r co mplete details o n settings and po ssible values, see the
Andro id Develo per Do cumentatio n. Remember that when yo u change values o n the Cam e ra.Param e t e rs instance
fro m Cam e ra.ge t Param e t e rs(), the settings are no t actually changed until yo u call Cam e ra.se t Param e t e rs
(Cam e ra.Param e t e rs param s) and pass the Cam e ra.Param e t e rs with the changed values.
In relatio n to live previews, the Cam e ra.Param e t e rs actually pro vides a list o f preview sizes fro m which yo u can
select to find the mo st appro priate preview size (as a Camera.Size o bject) fo r yo ur applicatio n.
There are lo ts o f useful features o n Cam e ra.Param e t e rs that yo u can use to implement a custo m camera. Fo r mo re
info rmatio n o n even mo re features, as well as API level limitatio ns, see the Andro id Develo per Do cumentatio n.
Checking for a Camera and Handling Multiple Cameras
If camera functio nality is o ptio nal in yo ur applicatio n and yo u need to check whether the device running yo ur
applicatio n has a camera, yo u can use the PackageManager to determine pro grammatically whether the device has a
camera. Yo u do that using this line o f co de fro m inside an Act ivit y:
OBSERVE: Using PackageManager to Check fo r a Camera
boolean hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
;
Act ivit y.ge t Package Manage r() retrieves the Package Manage r fo r the current Act ivit y.
Package Manage r.hasSyst e m Fe at ure (St ring), when passed Package Manage r.FEAT URE_CAMERA, will return
t rue if a camera is available.
In o ur pro ject, we did no t discuss multiple cameras, and ho w yo u can cho o se which camera to o pen and manipulate.
Unfo rtunately, it is difficult to test mo re than o ne camera o n the emulato r. Depending o n ho w yo ur AVD is set up, yo u
can specify a back o r a fro nt camera o r even bo th. Ho wever, when the emulato r actually runs, it will o nly have o ne
available, even if yo u set up two . This may change in future versio n o f the Andro id emulato r, but fo r no w, we'll just
review where yo u can get info rmatio n abo ut and gain access to a particular camera.
Each camera has an ID asso ciated with it. In o ur pro ject, we used Cam e ra.o pe n(), which takes no arguments and
o pens the back-facing camera by default. There is ano ther versio n o f the metho d, Cam e ra.o pe n(int ), which will o pen
the camera asso ciated with the integer ID passed. The ID is really just a zero -based index. If we kno w the index o f the
camera that we want, we just pass that to Cam e ra.o pe n(int ).
Getting the index o f the back vs. the fro nt camera invo lves a class called CameraInfo . Fo r each camera o n a device,
yo u can po pulate a Cam e raInf o instance that will tell yo u the directio n the camera faces and its o rientatio n. Device
cameras are essentially indexed by the Cam e ra class. Cam e ra.ge t Num be rOf Cam e ras will give yo u the to tal
number o f cameras o n the device. To grab the info rmatio n, yo u create a new Cam e raInf o instance and pass it to
Cam e ra.ge t Cam e raInf o (int , Cam e raInf o ). Afterwards, yo ur Cam e raInf o instance will have the relevant
info rmatio n. So to get the index o f the fro nt o r back-facing camera, we can iterate thro ugh the camera info rmatio n fo r
each camera until we co me acro ss o ne that is facing the directio n we want and return its index. So mething like this:
OBSERVE: Finding the Fro nt-facing Camera Sample Co de
int cameraIndex = -1;
int cameraCount = Camera.getNumberOfCameras();
for (int i = 0; i < cameraCount && cameraIndex == -1; i++) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
cameraIndex = i;
}
}
if (cameraIndex != -1) {}
Camera.open(cameraIndex);
}
If yo u want to find the index o f the back-facing camera, yo u can instead check that Cam e raInf o .f acing equals
Cam e raInf o .CAMERA_FACING_BACK.
Camera Features and the Android Manifest
Befo re submitting an applicatio n that uses camera features, yo u need to specify which features yo ur camera
applicatio n uses and also whether these features are o ptio nal, in the Andro id Manifest. To do that, yo u add <usesfeature/> tags and specify the andro id:name attribute fo r the particular feature. If the feature is o ptio nal, yo u also add the
andro id:required attribute and set it to false. Fo r example, fo r o ur applicatio n we can add this:
OBSERVE: Example Feature Tags fo r Andro idManifest.xml
<uses-feature android:name="android.hardware.camera" />
This lets Go o gle Play kno w that o ur applicatio n requires a camera. Go o gle Play will then filter o ut o ur applicatio n fo r
any devices bro wsing Go o gle Play that do no t have cameras. Yo u can see a list o f hardware feature descripto rs to use
with <uses-feature/> in the Andro id Develo per Do cumentatio n.
Wrapping Up
The Camera API is pretty co mplex and requires meticulo us wo rk to use pro perly. Fo rtunately, mo st o f the time yo u
wo n't need it, but if yo u do , the API is expansive eno ugh to allo w yo u to create a fully-featured Camera applicatio n.
While availability o f certain features varies acro ss Andro id API level and device, there are plenty o f ways fo r yo u to
assess a device's capabilities and take advantage o f them acco rdingly. Ho pefully, by no w yo u are co mfo rtable using
the Camera API to grab co ntro l o f camera reso urces, take images, sto re images, and (mo st impo rtantly) release the
camera back to the system. Yo u sho uld also no w kno w where to lo o k to find mo re advanced camera features to
leverage in yo ur applicatio n. See yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
BroadcastReceivers
Lesson Objectives
In this lesso n yo u will:
create a Bro adcastReceiver fo r receiving system events.
create a Bro adcastReceiver fo r receiving service events.
register a Bro adcastReceiver in the Andro id manifest.
register a Bro adcastReceiver pro grammatically in an activity.
use the Lo calBro adcastManager fo r sending and receiving events in the same applicatio n pro cess.
We've already wo rked with the Int e nt class with the Act ivit y class and Se rvice class. We can also use the Intent class to
pass events and messages to the Bro adcast Re ce ive r class. We can use Bro adcast Re ce ive r to listen fo r Int e nt s sent via
Co nt e xt .se ndBro adcast . A Bro adcast Re ce ive r can also listen to a number o f Int e nt s sent fo r system events, such as
when a device battery gets lo w, a SMS is received, o r when the user plugs in headpho nes. Bro adcast Re ce ive rs can also
receive Int e nt s sent fro m any Co nt e xt , including o ther applicatio ns (if the applicatio n allo ws o ther applicatio ns to receive
them).
In this lesso n, we'll discuss the basics o f using the Bro adcast Re ce ive r class, as well as the Lo calBro adcast Manage r.
Creating a BroadcastReceiver for System Events
Let get started with Bro adcast Re ce ive rs. First, we'll create a simple applicatio n that listens fo r when the device
receives a SMS and po ps up a little to ast message.
Create a new Andro id pro ject with this criteria:
Name the pro ject Bro adcast Re ce ive rs.
Use the package name co m .o re illyscho o l.andro id2.bro adcast Re ce ive rs.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
No w let's make o ur Bro adcast Receiver. Create a new class named SMSRe ce ive r that extends
andro id.co nt e nt .Bro adcast Re ce ive r. Make these changes to SMSRe ce ive r.java:
CODE TO TYPE: SMSReceiver.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
android.content.BroadcastReceiver;
android.content.Context;
android.content.Intent;
android.widget.Toast;
public class SMSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context arg0context, Intent arg1intent) {
// TODO Auto-generated method stub
Toast.makeText(context, "Received an SMS!", Toast.LENGTH_LONG).show();
}
}
Next, make a new permissio n in Andro idManif e st .xm l:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.broadcastReceivers"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<receiver
android:name=".SMSReceiver"
android:enabled="true" >
<intent-filter android:priority="999" >
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<!-<activity
android:name=".MainActivity"
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>
Save the mo dified files and run the applicatio n. Yo u wo n't actually see an activity po p up fo r the applicatio n, yo u'll just
see the ho me screen o n the emulato r.
Note
The next step remo ves this lesso n co ntent fro m yo ur screen, so take no te o f the next few steps to fo llo w
until yo u return to the lesso n using Windo w | Clo se Pe rspe ct ive .
We want to see what happens when the device receives an SMS message. We can simulate that in the emulato r. Select
Windo w | Ope n Pe rspe ct ive | Ot he r... and select the DDMS perspective, then go to the Em ulat o r Co nt ro l tab.
Fro m this tab, we can simulate events in the emulato r, such as pho ne/SMS events. In the T e le pho ny Act io ns panel,
type an inco ming number, select SMS, type a message and click Se nd:
The emulato r will simulate yo ur SMS message. Yo u see the typical no tificatio n fo r an SMS in the status bar. Click and
drag it do wn to see the message co ntent:
Let's review what we just did:
OBSERVE: SMSReceiver.java
...
public class SMSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received an SMS!", Toast.LENGTH_LONG).show();
}
}
Here we create a basic Bro adcast Re ce ive r subclass. The key to creating a Bro adcast Re ce ive r is to implement the
o nRe ce ive metho d. This is the callback fo r whatever event yo ur Bro adcast Re ce ive r has registered to receive. In
o ur o nRe ce ive , we po p up a to ast message saying that an SMS message was received by the device.
OBSERVE: Andro idManifest.xml
...
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<receiver
android:name=".SMSReceiver"
android:enabled="true" >
<intent-filter android:priority="999" >
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<!-<activity
android:name=".MainActivity"
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>
The first additio n to the Manifest is a t ag t o re que st pe rm issio n t o act ually re ce ive bro adcast s o f any SMS
m e ssage s.
We also co m m e nt e d o ut t he MainAct ivit y; since we aren't actually do ing anything in the MainAct ivit y, we
remo ved it fro m the applicatio n. We need to register o ur Bro adcast Re ce ive r with the system, so we added the
<re ce ive r> tag. The tag has two attributes: the nam e o f o ur Bro adcast Re ce ive r class and an e nable d value .
The <re ce ive r> tag also co ntains an <int e nt -f ilt e r>. By adding this <int e nt -f ilt e r> to the receiver registratio n, we
set up the Bro adcast Re ce ive r to receive Int e nt s with a particular actio n. We add a filter fo r the
andro id.pro vide r.T e le pho ny.SMS_RECEIVED actio n, which the system bro adcasts whenever it receives a SMS
message. Tho ugh it really do esn't matter in this applicatio n, we se t a prio rit y o n t he int e nt f ilt e r just to
demo nstrate ho w it can be used with a bro adcast receiver. By setting this value, yo u can enfo rce an o rder o r a
preference fo r the way bro adcasts are received. Fo r mo re info rmatio n o n o ptio nal attributes, see the do cumentatio n o n
the receiver tag and the intent-filter tag.
By registering o ur bro adcast receiver in the manifest, we allo w the system to co ntro l its lifecycle. We do no t have to
enable o r disable the bro adcast receiver explicitly fo r it to be able to receive intents. The Andro id OS will take care o f
running its co de in o ur applicatio n's pro cess.
The OS canno t guarantee the validity o f o ur Bro adcast Re ce ive r instance o utside o f the o nRe ce ive metho d. While
the system will treat the pro cess running the Bro adcast Re ce ive r.o nRe ce ive co de as a fo regro und pro cess fo r the
duratio n, if there are no o ther applicatio n co mpo nents running after o nRe ce ive finishes executio n, the pro cess will be
co nsidered empty and subsequently remo ved to free up reso urces. That's why the Andro id Do cumentatio n o n the
Bro adcastReceiver lifecycle warns against starting any asynchro no us o peratio ns fro m o nRe ce ive —the
Bro adcast Re ce ive r may no lo nger be valid when tho se o peratio ns return.
No w we have an applicatio n that uses a Bro adcast Re ce ive r to listen fo r and respo nd to a system event. There are
several o ther system events that yo u can leverage in this way, including when device bo o t co mpletes o r a package is
installed o r changes. In the next sectio n, we'll switch gears and use Bro adcast Re ce ive rs with o ur o wn applicatio n
services.
Creating a BroadcastReceiver for Service Events
Let's get started with Bro adcast Re ce ive rs and Se rvice s. Suppo se we want to make an applicatio n that listens fo r
and displays news headlines as they co me. We want to fo cus o n the Bro adcast Re ce ive r side o f things, so we'll just
create a dummy service fo r the headlines. Create a new class named He adline Se rvice that extends
andro id.app.Se rvice and make these changes:
CODE TO TYPE: HeadlineService.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
import
java.util.ArrayList;
java.util.Arrays;
java.util.Random;
java.util.Timer;
java.util.TimerTask;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class HeadlineService extends Service {
public static final String ACTION_HEADLINE = "com.ost.android2.action.HEADLINE_SENT";
public static final String EXTRA_HEADLINE = "com.ost.android2.extra.HEADLINE";
private static final int MINIMUM_HEADLINE_INTERVAL_SECONDS = 10;
private static final int MAXIUMUM_HEADLINE_INTERVAL_SECONDS = 25;
private static final int HEADLINE_INTERVAL_RANGE_SECONDS = MAXIUMUM_HEADLINE_INTERVAL
_SECONDS - MINIMUM_HEADLINE_INTERVAL_SECONDS + 1;
private static Random sRandom = new Random();
private static int getTimerLength() {
return (sRandom.nextInt(HEADLINE_INTERVAL_RANGE_SECONDS) + MINIMUM_HEADLINE_INTERVA
L_SECONDS) * 1000;
}
private Timer mTimer;
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public void onCreate() {
super.onCreate();
mTimer = new Timer();
mTimer.schedule(new BroadcastHeadlineTask(), getTimerLength());
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
}
}
private class BroadcastHeadlineTask
extends TimerTask {
private ArrayList<String> mHeadlines;
public BroadcastHeadlineTask() {
super();
mHeadlines = new ArrayList<String>(Arrays.asList(getResources().getStringArray(R.
array.headlines)));
}
@Override
public void run() {
Intent headlineIntent = new Intent(ACTION_HEADLINE);
headlineIntent.putExtra(EXTRA_HEADLINE, getHeadline());
sendBroadcast(headlineIntent);
if (mHeadlines.size() > 0) {
mTimer.schedule(new BroadcastHeadlineTask(), getTimerLength());
} else {
mTimer.cancel();
mTimer = null;
}
}
private String getHeadline() {
int index = sRandom.nextInt(mHeadlines.size());
return mHeadlines.remove(index);
}
}
}
No w o pen st rings.xm l and make these changes:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Broadcast Receivers</string>
<string name="action_settings">Settings</string>
<string name="hello">Hello World, MainActivity!</string>
<string name="headlines_label">Headlines</string>
<string-array name="headlines">
<item>Porcine Aeronautics Now Launching</item>
<item>Study Finds Apples and Oranges Are Actually Quite Alike</item>
<item>Ancient Tomb Discovered Contains Father of Lost Mummy</item>
<item>Four Pet Turtles Found inside Pizza Box in Sewers</item>
<item>Ashton Kocher Proclaims: I Caught Them All!</item>
<item>Feline/Canine Precipitation Falls over Florida</item>
<item>New Study: Ulnar Nerve, Not Humerus</item>
</string-array>
</resources>
We'll need a view, so o pen act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:id="@+id/headlines_label"
style="@android:style/TextAppearance.Large"
android:layout_width="wrap_contentmatch_parent"
android:layout_height="wrap_content"
android:background="#CCCCCC"
android:text="@string/headlines_label"
android:text="@string/hello_world" />
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLinearLayout>
No w, update the activity fo r the new view. Open MainAct ivit y.java and make these changes:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
import
import
import
import
android.app.ListActivity;
android.content.BroadcastReceiver;
android.content.Context;
android.content.Intent;
android.content.IntentFilter;
android.os.Bundle;
android.view.Menu;
android.widget.ArrayAdapter;
public class MainActivity extends ListActivity {
private NewsReceiver mNewsReceiver;
private ArrayAdapter<String> mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
startService(new Intent(MainActivity.this, HeadlineService.class));
mNewsReceiver = new NewsReceiver();
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(mNewsReceiver, new IntentFilter(HeadlineService.ACTION_HEADLINE));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mNewsReceiver);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Finally, we can edit o ur manifest so that it presents o nly the service we want. Open Andro idManif e st .xm l and make
these changes:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.broadcastReceivers"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".HeadlineService" />
<receiver
android:name=".SMSReceiver"
android:enabled="true" >
<intent-filter android:priority="999" >
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<!-<activity
android:name=".MainActivity"
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>
Save the mo dified files and run the applicatio n. Yo u see a screen with a few "Headlines" at the to p:
After yo u wait fo r a bit, headlines start to appear in the list at rando m intervals:
Here we start a dummy headline service that bro adcasts headlines via Int e nt s In o ur main activity, we use a
Bro adcast Re ce ive r to receive these bro adcasts and display them to the user. Let's take a clo ser lo o k at ho w we
acco mplished that:
OBSERVE: HeadlineService.java
...
public class HeadlineService extends Service {
...
private class BroadcastHeadlineTask extends TimerTask {
private ArrayList<String> mHeadlines;
public BroadcastHeadlineTask() {
super();
mHeadlines = new ArrayList<String>(Arrays.asList(getResources().getStringArray(R.
array.headlines)));
}
@Override
public void run() {
Intent headlineIntent = new Intent(ACTION_HEADLINE);
headlineIntent.putExtra(EXTRA_HEADLINE, getHeadline());
sendBroadcast(headlineIntent);
if (mHeadlines.size() > 0) {
mTimer.schedule(new BroadcastHeadlineTask(), getTimerLength());
} else {
mTimer.cancel();
mTimer = null;
}
}
private String getHeadline() {
int index = sRandom.nextInt(mHeadlines.size());
return mHeadlines.remove(index);
}
}
}
The He adline Se rvice wo n't be o ur main fo cus here, but let's just take a quick lo o k at ho w it wo rks. Once it's started,
the service sends o ut a string rando m ly se le ct e d f ro m an array o f st ring re so urce s, at so me rando m time
interval. It sends the headline o ut by calling Co nt e xt .se ndBro adcast , and passing an Int e nt o bje ct that co ntains
o ur custo m actio n ACT ION_HEADLINE:
OBSERVE: strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Broadcast Receiver</string>
<string name="action_settings">Settings</string>
<string name="headlines_label">Headlines</string>
<string-array name="headlines">
<item>Porcine Aeronautics Now Launching</item>
<item>Study Finds Apples and Oranges Are Actually Quite Alike</item>
<item>Ancient Tomb Discovered Contains Father of Lost Mummy</item>
<item>Four Pet Turtles Found inside Pizza Box in Sewers</item>
<item>Ashton Kocher Proclaims: I Caught Them All!</item>
<item>Feline/Canine Precipitation Falls over Florida</item>
<item>New Study: Ulnar Nerve, Not Humerus</item>
</string-array>
</resources>
In strings.xml, we add so me UI strings, including a st ring array o f f ake he adline s fo r o ur dummy service:
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<TextView
android:id="@+id/headlines_label"
style="@android:style/TextAppearance.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CCCCCC"
android:text="@string/headlines_label" />
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Fo r the layo ut o f o f o ur main activity in activity_main.xml, we add a T e xt Vie w at t he t o p f o r t he " He adline s" labe l
and a List Vie w t o ho ld t he re ce ive d he adline s:
OBSERVE: MainActivity.java
...
public class MainActivity extends ListActivity {
private NewsReceiver mNewsReceiver;
private ArrayAdapter<String> mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
startService(new Intent(MainActivity.this, HeadlineService.class));
mNewsReceiver = new NewsReceiver();
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(mNewsReceiver, new IntentFilter(HeadlineService.ACTION_HEADLINE));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mNewsReceiver);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
}
}
}
}
Mo st o f the new lo gic is in MainAct ivit y. We change the MainAct ivit y into a subclass o f
andro id.app.List Act ivit y. In o nCre at e , we se t up t he adapt e r f o r t he list . We also st art t he
He adline Se rvice . Finally, we inst ant iat e a bro adcast re ce ive r. Our subclass o f Bro adcast Re ce ive r is actually
an inner class, Ne wsRe ce ive r. We make o ur receiver an inner class so that it can have access to the activity and its
list. The Ne wsRe ce ive r will listen fo r headlines bro adcast by the He adline Se rvice and add them to the List Vie w. In
Ne wsRe ce ive r.o nRe ce ive , we pull the headline fro m the bro adcast Int e nt and then add it to the list adapter. To
register and unregister o ur Ne wsRe ce ive r, we call Co nt e xt .re gist e rRe ce ive r in o nRe sum e and
Co nt e xt .unre gist e rRe ce ive r in o nPause . We place them in o nRe sum e and o nPause so that when o ur
applicatio n is running in the backgro und, the Ne wsRe ce ive r will sto p receiving bro adcasts, which saves system
reso urces while the activity is in the backgro und.
WARNING
When yo u have registered a bro adcast receiver pro grammatically with
Co nt e xt .re gist e rRe ce ive r, yo u need to make sure to call Co nt e xt .unre gist e rRe ce ive r
when yo u finish with the receiver, o therwise, the receiver will be leaked and the OS will thro w an
erro r indicating this.
Also no te that, in o nRe ce ive , we verify the actio n o f the Int e nt . While we set up an Int e nt Filt e r to receive Int e nt s
where the actio n equals He adline Se rvice .ACT ION_HEADLINE, with Int e nt Filt e rs, an Int e nt passes if its actio n
matches any actio ns listed in the Int e nt Filt e r. If an Int e nt do es no t have any specified actio n, it passes
auto matically. Since it's po ssible fo r an actio n-less Int e nt to pass to o ur Bro adcast Re ce ive r, it's go o d practice to
verify the Int e nt actio n in o nRe ce ive .
OBSERVE: Andro idManifest.xml
...
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".HeadlineService" />
<activity
android:name=".MainActivity"
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>
Our final changes are in the Manifest. We remo ve the registratio n o f the SMS receiver fro m the last time and add in a
<se rvice > tag to declare o ur He adline Se rvice . We also add t he MainAct ivit y back int o t he m anif e st .
No w, we do n't have to limit a Bro adcast Re ce ive r to receiving just o ne actio n. We can change the Int e nt Filt e r so
that the Bro adcast Re ce ive r can receive multiple types o f actio ns. Let's try that no w. We'll make so me changes to o ur
applicatio n to include ano ther dummy service and have o ur Ne wsRe ce ive r handle bro adcasts fro m it as well.
Create a new class named T e m pe rat ure Se rvice that extends andro id.app.Se rvice , and make these changes:
CODE TO TYPE: TemperatureService.java
package com.oreillyschool.android2.broadcastReceivers;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class TemperatureService extends Service {
public static final String ACTION_TEMPERATURE_UPDATE = "com.ost.android2.action.TEMPE
RATURE_UPDATE";
public static final String EXTRA_TEMPERATURE = "com.ost.android2.extra.TEMPERATURE";
private static final int MINIMUM_UPDATE_INTERVAL_SECONDS = 5;
private static final int MAXIUMUM_UPDATE_INTERVAL_SECONDS = 10;
private static final int UPDATE_INTERVAL_RANGE_SECONDS = MAXIUMUM_UPDATE_INTERVAL_SEC
ONDS - MINIMUM_UPDATE_INTERVAL_SECONDS + 1;
private static final int MINIMUM_TEMPERATURE = 60;
private static final int TEMPERATURE_RANGE = 5;
private static Random sRandom = new Random();
private static int getTimerLength() {
return (sRandom.nextInt(UPDATE_INTERVAL_RANGE_SECONDS) + MINIMUM_UPDATE_INTERVAL_SE
CONDS) * 1000;
}
private Timer mTimer;
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public void onCreate() {
super.onCreate();
mTimer = new Timer();
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
}
}
private class TemperatureUpdateTask
extends TimerTask {
public TemperatureUpdateTask() {
super();
}
@Override
public void run() {
Intent temperatureIntent = new Intent(ACTION_TEMPERATURE_UPDATE);
int temperature = getTemperature();
temperatureIntent.putExtra(EXTRA_TEMPERATURE, temperature);
sendBroadcast(temperatureIntent);
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
private int getTemperature() {
int change = sRandom.nextInt(TEMPERATURE_RANGE);
return MINIMUM_TEMPERATURE + change;
}
}
}
Next, o pen st rings.xm l and make these changes:
CODE TO TYPE: strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Broadcast Receiver</string>
<string name="action_settings">Settings</string>
<string name="headlines_label">Headlines</string>
<string name="temperature_format">Temperature: %d ° F</string>
<string name="temperature_unavailable">Temperature: N/A</string>
<string-array name="headlines">
<item>Porcine Aeronautics Now Launching</item>
<item>Study Finds Apples and Oranges Are Actually Quite Alike</item>
<item>Ancient Tomb Discovered Contains Father of Lost Mummy</item>
<item>Four Pet Turtles Found inside Pizza Box in Sewers</item>
<item>Ashton Kocher Proclaims: I Caught Them All!</item>
<item>Feline/Canine Precipitation Falls over Florida</item>
<item>New Study: Ulnar Nerve, Not Humerus</item>
</string-array>
</resources>
No w o pen act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:id="@+id/headlines_label"
style="@android:style/TextAppearance.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CCCCCC"
android:text="@string/headlines_label" />
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent0dp" />
android:layout_weight="1" />
<TextView
android:id="@+id/temperature_text"
style="@android:style/TextAppearance.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#3300FF"
android:text="@string/temperature_unavailable" />
</LinearLayout>
Open MainAct ivit y.java and make these changes:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
import
import
import
import
android.app.ListActivity;
android.content.BroadcastReceiver;
android.content.Context;
android.content.Intent;
android.content.IntentFilter;
android.os.Bundle;
android.widget.ArrayAdapter;
android.widget.TextView;
public class MainActivity extends ListActivity {
private NewsReceiver mNewsReceiver;
private ArrayAdapter<String> mAdapter;
private TextView mTemperatureText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
mTemperatureText = (TextView) findViewById(R.id.temperature_text);
startService(new Intent(MainActivity.this, HeadlineService.class));
startService(new Intent(MainActivity.this, TemperatureService.class));
mNewsReceiver = new NewsReceiver();
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(mNewsReceiver, new IntentFilter(HeadlineService.ACTION_HEADLIN
E));
IntentFilter newsFilter = new IntentFilter();
newsFilter.addAction(HeadlineService.ACTION_HEADLINE);
newsFilter.addAction(TemperatureService.ACTION_TEMPERATURE_UPDATE);
registerReceiver(mNewsReceiver, newsFilter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mNewsReceiver);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
} else if (intent.getAction().equals(TemperatureService.ACTION_TEMPERATURE_
UPDATE)) {
int temperature = intent.getIntExtra(TemperatureService.EXTRA_TEMPERATU
RE,
Integer.MIN_VALUE);
mTemperatureText.setText(temperature != Integer.MIN_VALUE
? getString(R.string.temperature_format, temperature)
: getString(R.string.temperature_unavailable));
}
}
}
}
Finally, make this change to Andro idManif e st .xm l:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.broadcastReceivers;"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".HeadlineService" />
<service android:name=".TemperatureService" />
<activity
android:name=".MainActivity"
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>
Save all the mo dified files and run the applicatio n. When the applicatio n starts, yo u see a blank headline list like befo re.
Yo u also see a blue bo x at the bo tto m with a dummy temperature reading. When the applicatio n is launched, the value
is "N/A" because o ur applicatio n has no t received any temperature bro adcasts yet.
After a while, the headlines po pulate like they did befo re. Eventually the temperature will get a valid value and the
o ccasio nal update will take place:
So no w we have two services bro adcasting, o ur applicatio n receives tho se bro adcasts, and updates the UI. Let's take
a clo ser lo o k at ho w we did that.
OBSERVE: TemperatureService.java
...
private class TemperatureUpdateTask
extends TimerTask {
public TemperatureUpdateTask() {
super();
}
@Override
public void run() {
Intent temperatureIntent = new Intent(ACTION_TEMPERATURE_UPDATE);
int temperature = getTemperature();
temperatureIntent.putExtra(EXTRA_TEMPERATURE, temperature);
sendBroadcast(temperatureIntent);
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
private int getTemperature() {
int change = sRandom.nextInt(TEMPERATURE_RANGE);
return MINIMUM_TEMPERATURE + change;
}
}
}
The T e m pe rat ure Se rvice is similar to the He adline Se rvice , o nly instead o f bro adcasting an Int e nt with a rando m
headline, the T e m pe rat ure Se rvice bro adcasts a rando m t e m pe rat ure re ading.
OBSERVE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.broadcastReceivers"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".HeadlineService" />
<service android:name=".TemperatureService" />
<activity
android:name=".MainActivity"
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>
In the Manifest, we add the de clarat io n f o r t he T e m pe rat ure Se rvice .
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<TextView
android:id="@+id/headlines_label"
style="@android:style/TextAppearance.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CCCCCC"
android:text="@string/headlines_label" />
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/temperature_text"
style="@android:style/TextAppearance.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#3300FF"
android:text="@string/temperature_unavailable" />
</LinearLayout>
In the main activity's layo ut, we add a T e xt Vie w at the bo tto m and change the List Vie w to fill the space abo ve it in the
LinearLayo ut.
OBSERVE: MainActivity.java
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
mTemperatureText = (TextView) findViewById(R.id.temperature_text);
startService(new Intent(MainActivity.this, HeadlineService.class));
startService(new Intent(MainActivity.this, TemperatureService.class));
mNewsReceiver = new NewsReceiver();
}
@Override
protected void onResume() {
super.onResume();
IntentFilter newsFilter = new IntentFilter();
newsFilter.addAction(HeadlineService.ACTION_HEADLINE);
newsFilter.addAction(TemperatureService.ACTION_TEMPERATURE_UPDATE);
registerReceiver(mNewsReceiver, newsFilter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mNewsReceiver);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
} else if (intent.getAction().equals(TemperatureService.ACTION_TEMPERATURE_UPDATE
)) {
int temperature = intent.getIntExtra(TemperatureService.EXTRA_TEMPERATURE,
Integer.MIN_VALUE);
mTemperatureText.setText(temperature != Integer.MIN_VALUE
? getString(R.string.temperature_format, temperature)
: getString(R.string.temperature_unavailable));
}
}
}
}
Again, the majo rity o f o ur wo rk and changes are in the MainAct ivit y class. In o nCre at e , we add a call to st art t he
T e m pe rat ure Se rvice . In o nRe sum e , we change ho w we instantiate the Int e nt Filt e r. We add t wo act io ns t o
t he Int e nt Filt e r: He adline Se rvice .ACT ION_HEADLINE f o r t he He adline Se rvice and
T e m pe rat ure Se rvice .ACT ION_T EMPERAT URE_UPDAT E f o r t he t e m pe rat ure . An Int e nt with either actio n
will pass the Int e nt Filt e r and pass to o nRe ce ive . When it receives a bro adcast, o nRe ce ive che cks t he act io n
and either updat e s t he list wit h a he adline o r updat e s t he t e m pe rat ure t e xt .
So no w we can handle a number o f different kinds o f Int e nt s with the same Bro adcast Re ce ive r. We can also use a
Bro adcast Re ce ive r to handle system events. When Co nt e xt .se ndBro adcast is called, it's po ssible fo r any
registered Bro adcast Re ce ive r to receive the Int e nt . This includes Bro adcast Re ce ive rs in o ther applicatio ns. In
o ther wo rds, Co nt e xt .se ndBro adcast is sent glo bally acro ss the system.
Yo u will o ften find that yo u just need to bro adcast between co mpo nents in the same applicatio n. If this is the case,
there is a better way to send and receive bro adcasts that sho uld remain lo cal to an applicatio n:
Lo calBro adcast Manage r. We'll lo o k at that next.
Using the LocalBroadcastManager
The Lo calBro adcast Manage r is a class that has its o wn implementatio n o f se ndBro adcast , re gist e rRe ce ive r,
and unre gist e rRe ce ive r. Int e nt s bro adcasted via the Lo calBro adcast Manage r can o nly be received by
Bro adcast Re ce ive rs that were registered with the Lo calBro adcast Manage r. Also , Bro adcast Re ce ive rs
registered with the Lo calBro adcast Manage r canno t receive Int e nt s sent by Co nt e xt .se ndBro adcast .
If yo u o nly need bro adcasting between co mpo nents in the same applicatio n, use the Lo calBro adcast Manage r.
Using Lo calBro adcast Manage r prevents data fro m yo ur applicatio n fro m being bro adcast to o ther applicatio ns and
it prevents o ther applicatio ns bro adcasting to yo urs. It's also mo re efficient than using a Co nt e xt to bro adcast.
Let's add Lo calBro adcast Manage r to o ur pro ject.
Open T e m pe rat ure Se rvice .java and make these changes:
CODE TO TYPE: TemperatureService.java
package com.oreillyschool.android2.broadcastReceivers;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import
import
import
import
android.app.Service;
android.content.Intent;
android.os.IBinder;
android.support.v4.content.LocalBroadcastManager;
public class TemperatureService extends Service {
public static final String ACTION_TEMPERATURE_UPDATE = "com.ost.android2.action.TEMPE
RATURE_UPDATE";
public static final String EXTRA_TEMPERATURE = "com.ost.android2.extra.TEMPERATURE";
private static final int MINIMUM_UPDATE_INTERVAL_SECONDS = 5;
private static final int MAXIUMUM_UPDATE_INTERVAL_SECONDS = 10;
private static final int UPDATE_INTERVAL_RANGE_SECONDS = MAXIUMUM_UPDATE_INTERVAL_SEC
ONDS - MINIMUM_UPDATE_INTERVAL_SECONDS + 1;
private static final int MINIMUM_TEMPERATURE = 60;
private static final int TEMPERATURE_RANGE = 5;
private static Random sRandom = new Random();
private static int getTimerLength() {
return (sRandom.nextInt(UPDATE_INTERVAL_RANGE_SECONDS) + MINIMUM_UPDATE_INTERVAL_SE
CONDS) * 1000;
}
private Timer mTimer;
@Override
public IBinder onBind(Intent arg0) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mTimer = new Timer();
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
}
}
private class TemperatureUpdateTask
extends TimerTask {
public TemperatureUpdateTask() {
super();
}
@Override
public void run() {
Intent temperatureIntent = new Intent(ACTION_TEMPERATURE_UPDATE);
int temperature = getTemperature();
temperatureIntent.putExtra(EXTRA_TEMPERATURE, temperature);
sendBroadcast(temperatureIntent);
LocalBroadcastManager.getInstance(TemperatureService.this).sendBroadcast(temperat
ureIntent);
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
private int getTemperature() {
int change = sRandom.nextInt(TEMPERATURE_RANGE);
return MINIMUM_TEMPERATURE + change;
}
}
}
No w o pen He adline Se rvice .java and make these changes:
CODE TO TYPE: HeadlineService.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
import
java.util.ArrayList;
java.util.Arrays;
java.util.Random;
java.util.Timer;
java.util.TimerTask;
import
import
import
import
android.app.Service;
android.content.Intent;
android.os.IBinder;
android.support.v4.content.LocalBroadcastManager;
public class HeadlineService extends Service {
...
private class BroadcastHeadlineTask
extends TimerTask {
public BroadcastHeadlineTask() {
super();
}
@Override
public void run() {
Intent headlineIntent = new Intent(ACTION_HEADLINE);
String headline = getHeadline();
headlineIntent.putExtra(EXTRA_HEADLINE, headline);
sendBroadcast(headlineIntent);
LocalBroadcastManager.getInstance(HeadlineService.this).sendBroadcast(headlineInt
ent);
if (mHeadlines.size() > 0) {
mTimer.schedule(new BroadcastHeadlineTask(), getTimerLength());
} else {
mTimer.cancel();
mTimer = null;
}
}
}
...
Finally, make these changes to MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.broadcastReceivers;
import
import
import
import
import
import
import
import
import
android.app.ListActivity;
android.content.BroadcastReceiver;
android.content.Context;
android.content.Intent;
android.content.IntentFilter;
android.os.Bundle;
android.support.v4.content.LocalBroadcastManager;
android.widget.ArrayAdapter;
android.widget.TextView;
public class MainActivity extends ListActivity {
private NewsReceiver mNewsReceiver;
private ArrayAdapter<String> mAdapter;
private TextView mTemperatureText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
mTemperatureText = (TextView) findViewById(R.id.temperature_text);
startService(new Intent(MainActivity.this, HeadlineService.class));
startService(new Intent(MainActivity.this, TemperatureService.class));
mNewsReceiver = new NewsReceiver();
IntentFilter newsFilter = new IntentFilter();
newsFilter.addAction(HeadlineService.ACTION_HEADLINE);
newsFilter.addAction(TemperatureService.ACTION_TEMPERATURE_UPDATE);
LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(mNewsReceiver
, newsFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(MainActivity.this).unregisterReceiver(mNewsReceiv
er);
}
@Override
protected void onResume() {
super.onResume();
IntentFilter newsFilter = new IntentFilter();
newsFilter.addAction(HeadlineService.ACTION_HEADLINE);
newsFilter.addAction(TemperatureService.ACTION_TEMPERATURE_UPDATE);
registerReceiver(mNewsReceiver, newsFilter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mNewsReceiver);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
} else if (intent.getAction().equals(TemperatureService.ACTION_TEMPERATURE_UPDATE
)) {
int temperature = intent.getIntExtra(TemperatureService.EXTRA_TEMPERATURE,
Integer.MIN_VALUE);
mTemperatureText.setText(temperature != Integer.MIN_VALUE
? getString(R.string.temperature_format, temperature)
: getString(R.string.temperature_unavailable));
}
}
}
}
Save all mo dified files and run the applicatio n. As befo re, yo u initially see a blank screen and temperature field, and the
screen eventually fills with bro adcasts as they enter. If yo u had hit the "Ho me" butto n o r in so me o ther way put the
activity in the backgro und, the activity wo uld have sto pped receiving headline bro adcasts because we unregistered the
Bro adcast Re ce ive r in o nPause . So , yo u wo uld have co nceivably missed bro adcasts while the activity was in the
backgro und. This time even if the applicatio n is in the backgro und, yo u will be able to receive bro adcasts.
Let's review o ur co de to examine ho w and why we switched to Lo calBro adcast Manage r:
OBSERVE: TemperatureService.java
...
private class TemperatureUpdateTask extends TimerTask {
public TemperatureUpdateTask() {
super();
}
@Override
public void run() {
Intent temperatureIntent = new Intent(ACTION_TEMPERATURE_UPDATE);
int temperature = getTemperature();
temperatureIntent.putExtra(EXTRA_TEMPERATURE, temperature);
LocalBroadcastManager.getInstance(TemperatureService.this).sendBroadcast(temperat
ureIntent);
mTimer.schedule(new TemperatureUpdateTask(), getTimerLength());
}
private int getTemperature() {
int change = sRandom.nextInt(TEMPERATURE_RANGE);
return MINIMUM_TEMPERATURE + change;
}
}
In T e m pe rat ure Se rvice .java, switching to the Lo calBro adcast Manage r required replacing the o riginal call to
Co nt e xt .se ndBro adcast with a call to Lo calBro adcast Manage r. The Lo calBro adcast Manage r is actually a
singleto n, so first we use Lo calBro adcast Manage r.ge t Inst ance (andro id.co nt e nt .Co nt e xt ) to get a reference
to it and then call Lo calBro adcast Manage r.se ndBro adcast with the same Int e nt as befo re:
OBSERVE: HeadlineService.java
...
private class BroadcastHeadlineTask extends TimerTask {
public BroadcastHeadlineTask() {
super();
}
@Override
public void run() {
Intent headlineIntent = new Intent(ACTION_HEADLINE);
String headline = getHeadline();
headlineIntent.putExtra(EXTRA_HEADLINE, headline);
LocalBroadcastManager.getInstance(HeadlineService.this).sendBroadcast(headlineInt
ent);
if (mHeadlines.size() > 0) {
mTimer.schedule(new BroadcastHeadlineTask(), getTimerLength());
} else {
mTimer.cancel();
mTimer = null;
}
}
private String getHeadline() {
int index = sRandom.nextInt(mHeadlines.size());
return mHeadlines.remove(index);
}
}
Fo r He adline Se rvice .java, we make the same changes as fo r T e m pe rat ure Se rvice .java: we remo ve the previo us
call to Co nt e xt .se ndBro adcast and replace it with a call to Lo calBro adcast Manage r.se ndBro adcast , keeping
the Int e nt the same as befo re:
OBSERVE: MainActivity.java
...
public class MainActivity extends ListActivity {
private NewsReceiver mNewsReceiver;
private ArrayAdapter<String> mAdapter;
private TextView mTemperatureText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
setListAdapter(mAdapter);
mTemperatureText = (TextView) findViewById(R.id.temperature_text);
startService(new Intent(MainActivity.this, HeadlineService.class));
startService(new Intent(MainActivity.this, TemperatureService.class));
mNewsReceiver = new NewsReceiver();
IntentFilter newsFilter = new IntentFilter();
newsFilter.addAction(HeadlineService.ACTION_HEADLINE);
newsFilter.addAction(TemperatureService.ACTION_TEMPERATURE_UPDATE);
LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(mNewsReceiver
, newsFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(MainActivity.this).unregisterReceiver(mNewsReceiv
er);
}
private class NewsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(HeadlineService.ACTION_HEADLINE)) {
mAdapter.add(intent.getStringExtra(HeadlineService.EXTRA_HEADLINE));
mAdapter.notifyDataSetChanged();
} else if (intent.getAction().equals(TemperatureService.ACTION_TEMPERATURE_UPDATE
)) {
int temperature = intent.getIntExtra(TemperatureService.EXTRA_TEMPERATURE,
Integer.MIN_VALUE);
mTemperatureText.setText(temperature != Integer.MIN_VALUE
? getString(R.string.temperature_format, temperature)
: getString(R.string.temperature_unavailable));
}
}
}
}
Fo r MainAct ivit y.java, we made so me mo re significant changes. We remo ved o ur implementatio ns o f o nRe sum e
and o nPause . Instead, we m o ve d cre at io n o f t he Int e nt Filt e r and t he Ne wsRe ce ive r t o o nCre at e . We call
Lo calBro adcast Manage r.re gist e rRe ce ive r in o nCreate as well. To make sure that we aren't in danger o f leaking
the Ne wsRe ce ive r instance, we put a matching call to Lo calBro adcast Manage r.unre gist e rRe ce ive r in
o nDe st ro y. Since we placed the register and unregister calls in o nCre at e and o nDe st ro y instead o f in o nRe sum e
and o nPause , o ur activity was still able to receive headline and temperature bro adcasts even when it was in the
backgro und.
Yo u have so me o ptio ns fo r ho w and where yo u register a Bro adcast Re ce ive r. Yo ur decisio n depends o n yo ur
applicatio n needs, and perfo rmance and security co nsideratio ns. If yo u are keeping bro adcasts limited to yo ur o wn
applicatio n needs, and perfo rmance and security co nsideratio ns. If yo u are keeping bro adcasts limited to yo ur o wn
applicatio n co mpo nents, Lo calBro adcast Manage r is the best o ptio n fo r yo u.
Wrapping Up
In this lesso n, we went thro ugh the basics o f utilizing the Bro adcast Re ce ive r class. It's relatively straightfo rward to
use and pro vides much flexibility. Yo u can cho o se to register yo ur receiver in the Andro id manifest o r in co de. Yo u can
cho o se to use the Co nt e xt bro adcast metho ds o r use Lo calBro adcast Manage r. Yo u can listen fo r bro adcasts
fro m yo ur o wn applicatio n, fro m o ther applicatio ns, and fro m the system. Yo u can use an inner class fo r yo ur
Bro adcast Re ce ive r o r a stand-alo ne.
The Bro adcast Re ce ive r is a po werful to o l fo r co mmunicatio n, but the trick is to use the o ptio ns and
implementatio ns that best fit yo ur applicatio n, and make sure that yo u always co nsider efficiency and security. Also ,
co nsider the lifecycle o f Bro adcast Re ce ive r instances, particularly when yo u register in the Andro id manifest.
Well, that sho uld get yo u a go o d start o n Bro adcast Re ce ive rs. See yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Media: Audio
Lesson Objectives
At the end o f this lesso n, yo u'll be able to :
create a MediaPlayer and play an audio file.
understand the different states o f the MediaPlayer.
utilize the MediaPlayer co rrectly within the Activity lifecycle.
respo nd to MediaPlayer events.
In this lesso n, we'll discuss ho w yo u can play and manage audio with the Andro id MediaPlayer. The MediaPlayer is used to
play bo th audio and video fro m either files o r streams. The MediaPlayer is essentially a state machine. The vario us o peratio ns
that yo u can call o n a MediaPlayer send it into o ne o f many states. The o peratio ns that yo u can call are dependent o n the
current state. Fo r a go o d diagram sho wing the vario us MediaPlayer states and the metho ds mo ve yo u between each state, take
a lo o k at the Andro id Develo per Do cumentatio n fo r the MediaPlayer.
The MediaPlayer is flexible, allo wing yo u to play lo cal files as well as HTTP streaming. It pro vides basic playback functio nality:
start, sto p, pause, seek, and lo o p. There are also several callbacks that allo w yo ur applicatio n to react to different events like
buffering, seeking, co mpletio n, and so o n.
Creating a MediaPlayer and Playing an Audio File
Create a new Andro id pro ject with the fo llo wing criteria.
Name the pro ject Me diaAudio .
Use the package name co m .o re illyscho o l.andro id2.m e diaaudio .
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
First, we'll start setting up the UI. Mo dify st rings.xm l as sho wn:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MediaPlayer Audio</string>
<string name="action_settings">Settings</string>
<string name="hello_world">Hello World, MainActivity!</string>
<string name="start_button_label">Start</string>
<string name="stop_button_label">Stop</string>
<string name="song_01_info">"Persephone" by snowflake (feat. Vidian, Dimitri Artemenk
o)\nhttp://ccmixter.org/files/snowflake/22364\n\nLicensed under a Creative Commons lice
nse:\nhttp://creativecommons.org/licenses/by/2.5/</string>
<string name="error_io_message">There was a problem opening this file.</string>
<string name="error_illegal_state_start_message">Tried to start MediaPlayer in illega
l state.</string>
</resources>
Next, mo dify act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:padding="15dp"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<TextView
android:id="@+id/song_info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginBottom="15dp"
android:textSize="4pt"
android:text="@string/song_01_info" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/start_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_button_label" />
<Button
android:id="@+id/stop_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/stop_button_label" />
</LinearLayout>
</RelativeLinearLayout>
No w let's add an audio file to o ur pro ject to play. To do wnlo ad the audio file, right-click o n the link belo w and save the
file to the pro ject's /re s/raw fo lder:
Do wnlo ad Audio File.
Note
The pro ject fo lders are lo cated o n the V drive in the /wo rkspace fo lder; the full path where yo u'll save the
image is: V:\wo rkspace \Me diaAudio \re s\raw. Yo u may need to create the /raw fo lder.
Finally, make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediaaudio;
import java.io.IOException;
import
import
import
import
import
import
import
import
android.app.Activity;
android.media.MediaPlayer;
android.os.Bundle;
android.view.Menu;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.Toast;
public class MainActivity extends Activity {
private MediaPlayer mMediaPlayer;
private Button mStartButton;
private Button mStopButton;
public void start() {
mMediaPlayer.start(); // MediaPlayer is started.
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
}
public void stop() {
mMediaPlayer.stop(); // MediaPlayer is stopped.
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
private OnClickListener mStartOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
start();
}
};
private OnClickListener mStopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stop();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
// Disabling start/stop buttons before MediaPlayer gets set up, in case it doesn't
set up properly.
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
mMediaPlayer = new MediaPlayer(); // MediaPlayer is idle.
try {
mMediaPlayer.setDataSource(getResources().openRawResourceFd(R.raw.persephone_by_s
nowflake).getFileDescriptor()); // MediaPlayer is initialized.
mMediaPlayer.prepare(); // MediaPlayer is prepared.
mStartButton.setEnabled(true);
} catch (IOException ioe) {
Toast.makeText(this, R.string.error_io_message, Toast.LENGTH_LONG);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Save all mo dified files and run the applicatio n. Yo u see so me text (which co ntains attributio n fo r the audio we're using)
and St art and St o p butto ns. If yo u click St art , the music starts to play. After the music starts to play, yo u will be able
to click St o p to sto p the music.
Note
In this lesso n, we'll use a lo t o f audio and video files. If yo u try to run the applicatio n in the emulato r and
yo u get an INST ALL_FAILED_INSUFFICIENT _ST ORAGE erro r in the co nso le, yo u need to increase
the amo unt o f sto rage in the emulato r. To do this, select Run | De bug Co nf igurat io ns, .... In the
De bug Co nf igurat io ns windo w, select the T arge t tab. In the Emulato r Launch Parameters sectio n, in
the Additio nal Emulato r Co mmand Line Optio ns bo x, enter -part it io n-size 10 24 . The partitio n size is in
megabytes; it's go o d practice to make sure that it is twice as big as yo ur APK size.
Let's take a clo ser lo o k at the Me diaPlaye r co de in MainAct ivit y:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
private MediaPlayer mMediaPlayer;
private Button mStartButton;
private Button mStopButton;
public void start() {
mMediaPlayer.start(); // MediaPlayer is started.
tton.setEnabled(false);
mStopButton.setEnabled(true);
}
public void stop() {
mMediaPlayer.stop(); // MediaPlayer is stopped.
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
private OnClickListener mStartOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
start()
}
};
private OnClickListener mStopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stop();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
// Disabling start/stop buttons before MediaPlayer gets set up, in case it doesn't
set up properly.
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
mMediaPlayer = new MediaPlayer(); // MediaPlayer is idle.
try {
mMediaPlayer.setDataSource(getResources().openRawResourceFd(R.raw.persephone_by_s
nowflake).getFileDescriptor()); // MediaPlayer is initialized.
mMediaPlayer.prepare(); // MediaPlayer is prepared.
mStartButton.setEnabled(true);
} catch (IOException ioe) {
Toast.makeText(this, R.string.error_io_message, Toast.LENGTH_LONG);
}
}
}
In the o nCre at e () metho d, we inst ant iat e an inst ance o f t he Me diaPlaye r using t he ne w ke ywo rd. After we
instantiate the MediaPlayer, it is in the Idle state. Next, we se t t he dat a so urce f o r t he Me diaPlaye r by calling
se t Dat aSo urce wit h a File De script o r o bje ct cre at e d f ro m t he id o f t he raw re so urce we save d t o t he
pro je ct : R.raw.pe rse pho ne _by_sno wf lake . After setting the data so urce, the MediaPlayer is in the Initialized state.
The MediaPlayer, ho wever, is still no t set up fo r playback. Fo r that, we call pre pare () o n t he Me diaPlaye r. No w, the
MediaPlayer is in a state that can play the audio file, so we e nable t he St art but t o n. We wired up the St art but t o n
t o call st art () and the St o p but t o n t o call st o p(). These metho ds do the wo rk o f calling Me diaPlaye r.st art ()
and Me diaPlaye r.st o p(), as well as enabling/disabling the "Start"/"Sto p" butto ns as appro priate. When we call
Me diaPlaye r.st art , the MediaPlayer transitio ns to the Started state and no w plays the file. So just to review, o ur
MediaPlayer went fro m Idle (o n creatio n) to Initialized (after we set the data so urce) to Prepared (after we called
MediaPlayer.prepare()) to Started (after we call MediaPlayer.start()).
There's a pro blem with o ur co de tho ugh. If yo u click St art again after clicking St o p, the music will no t replay. In fact, if
yo u go to Lo gCat, yo u'll see an erro r messages:
After yo u call Me diaPlaye r.st o p(), the MediaPlayer enters into the Sto pped state. In this state, the MediaPlayer is no
lo nger prepared fo r playback. In o rder to start the playback again, we need to call Me diaPlaye r.pre pare () again
befo re calling Me diaPlaye r.st art (). Keep in mind that in music apps there is a co nventio n that differentiates
"sto pping" fro m "pausing": "sto pping" wo uld mo ve the play po sitio n back to the beginning, whereas "pausing" merely
halts playback and maintains the same po sitio n in the so ng. The Me diaPlaye r.st o p() metho d do es no t mo ve the
Me diaPlaye r's po sitio n back to the beginning; it mo ves the MediaPlayer's state to the Sto pped state. MediaPlayer
do es have a pause () metho d which sto ps playback, maintains the current po sitio n, and mo ves the MediaPlayer to the
Paused state. Ho wever, the MediaPlayer can mo ve back to the Started state fro m the Paused just by calling
Me diaPlaye r.st art (), rather than having to call Me diaPlaye r.pre pare () again. Be aware o f the difference between
the co nventio n o f "sto p" in a music player and what actually happens after Me diaPlaye r.st o p(). As we change o ur
co de, let's use Me diaPlaye r.pause ().
Also , if yo u start the music and go to the ho me screen, the music still plays. Fo r this MediaPlayer, let's say we want the
music to sto p when the applicatio n isn't visible. We'll need to make a few mo re changes to take make that happen.
Handling MediaPlayer State and the Activity Lifecycle
Our applicatio n do esn't handle changing o f o rientatio n o r pausing/resuming yet. It is vital that we kno w ho w to handle
these changes tho ugh, because they can have a big impact o n the MediaPlayer and its state, so in this sectio n, we'll
impro ve o ur player's functio n by managing these scenario s.
Let's change fro m a Start/Sto p co ncept to a mo re co nventio nal Play/Pause/Sto p co ncept.
Make these changes to st rings.xm l:
CODE TO TYPE: strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MediaPlayer Audio</string>
<string name="action_settings">Settings</string>
<string name="start_button_label">StartPlay</string>
<string name="stop_button_label">Stop</string>
<string name="pause_button_label">Pause</string>
<string name="song_01_info">"Persephone" by snowflake (feat. Vidian, Dimitri Arteme
nko)\nhttp://ccmixter.org/files/snowflake/22364\n\nLicensed under a Creative Commons li
cense:\nhttp://creativecommons.org/licenses/by/2.5/</string>
<string name="error_io_message">There was a problem opening this file.</string>
<string name="error_illegal_state_start_message">Tried to start MediaPlayer in ille
gal state.</string>
</resources>
No w make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediaaudio;
import java.io.IOException;
import
import
import
import
import
import
import
android.app.Activity;
android.media.MediaPlayer;
android.os.Bundle;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.Toast;
public class MainActivity extends Activity {
private MediaPlayer mMediaPlayer;
private Button mStartButton;
private Button mStopButton;
private boolean mWasPlaying;
public void start() {
public void play() {
mMediaPlayer.start(); // MediaPlayer is started.
mStartButton.setText(getResources().getString(R.string.pause_button_label));
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
}
public void pause() {
mMediaPlayer.pause(); // MediaPlayer is paused.
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
public void stop() {
mMediaPlayer.stop(); // MediaPlayer is stopped.
mMediaPlayer.pause(); // MediaPlayer is paused.
mMediaPlayer.seekTo(0);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
private OnClickListener mStartOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
start();
if (mMediaPlayer.isPlaying()) {
pause();
} else {
play();
}
}
};
private OnClickListener mStopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stop();
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
mStartButton.setText(getResources().getString(R.string.start_button_label));
// Disabling start/stop buttons before MediaPlayer gets set up,
// in case it doesn't set up properly.
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
mMediaPlayer = new MediaPlayer(); // MediaPlayer is idle.
try {
mMediaPlayer.setDataSource(getResources().openRawResourceFd(R.raw.persephone_by_s
nowflake).getFileDescriptor()); // MediaPlayer is initialized.
mMediaPlayer.prepare(); // MediaPlayer is prepared.
mStartButton.setEnabled(true);
} catch (IOException ioe) {
Toast.makeText(this, R.string.error_io_message, Toast.LENGTH_LONG);
}
mMediaPlayer = MediaPlayer.create(this, R.raw.persephone_by_snowflake); // MediaPla
yer is prepared.
if (mMediaPlayer != null) {
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
} else {
mStartButton.setEnabled(false);
}
}
@Override
protected void onPause() {
super.onPause();
mWasPlaying = mMediaPlayer.isPlaying();
if (mWasPlaying) {
mMediaPlayer.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mWasPlaying) {
play();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isPlaying", mMediaPlayer.isPlaying());
outState.putInt("progress", mMediaPlayer.getCurrentPosition());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mWasPlaying = savedInstanceState.getBoolean("isPlaying");
mMediaPlayer.seekTo(savedInstanceState.getInt("progress"));
if (!mWasPlaying && savedInstanceState.getInt("progress") > 0) {
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
}
}
No w save the mo dified files and run the applicatio n. Instead o f St art and St o p butto ns, we no w have Play and St o p
butto ns. When yo u press Play, the music begins to play and the Play butto n changes to Pause . Pressing Pause will
halt the playback at its current po sitio n. This butto n will co ntinue to to ggle back and fo rth between Play and Pause as
yo u click. If yo u press St o p, instead o f just pausing the music, the playback sto ps and the next time yo u press Play
the audio file plays fro m the beginning.
When yo u ro tate the emulato r, the applicatio n maintains the co rrect state; if the audio was playing befo re the ro tatio n, it
will play after the ro tatio n. If the audio was sto pped o r paused, it will be the same after the ro tatio n. Similarly, if yo u go
to the ho me screen and then co me back, the applicatio n sho uld be in the same state as when yo u left.
Let's go o ver the changes we just made:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
private MediaPlayer mMediaPlayer;
private Button mStartButton;
private Button mStopButton;
private boolean mWasPlaying;
public void play() {
mMediaPlayer.start(); // MediaPlayer is started.
mStartButton.setText(getResources().getString(R.string.pause_button_label));
mStopButton.setEnabled(true);
}
public void pause() {
mMediaPlayer.pause(); // MediaPlayer is paused.
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
public void stop() {
mMediaPlayer.pause(); // MediaPlayer is paused.
mMediaPlayer.seekTo(0);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStopButton.setEnabled(false);
mMediaPlayer = MediaPlayer.create(this, R.raw.persephone_by_snowflake); // MediaPla
yer is prepared.
if (mMediaPlayer != null) {
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
} else {
mStartButton.setEnabled(false);
}
}
@Override
protected void onPause() {
super.onPause();
mWasPlaying = mMediaPlayer.isPlaying();
if (mWasPlaying) {
mMediaPlayer.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mWasPlaying) {
play();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isPlaying", mMediaPlayer.isPlaying());
outState.putInt("progress", mMediaPlayer.getCurrentPosition());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mWasPlaying = savedInstanceState.getBoolean("isPlaying");
mMediaPlayer.seekTo(savedInstanceState.getInt("progress"));
if (!mWasPlaying &&savedInstanceState.getInt("progress") > 0) {
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
}
}
In o nCre at e , instead o f creating the MediaPlayer using ne w and manually preparing the MediaPlayer and catching
exceptio ns, we use t he st at ic cre at e m e t ho d. Since we're o nly playing o ne reso urce audio file,
Me diaPlaye r.cre at e simplifies the wo rk fo r us. It takes care o f initializing and preparing the audio file, and if there are
any pro blems, it returns null. Since the MediaPlayer relies o n state, yo u need to be aware that a MediaPlayer returned
by the static Me diaPlaye r.cre at e metho d is already in the Prepared state. Also , because we use the
Me diaPlaye r.cre at e metho d, the MediaPlayer o bject co uld po tentially be null, so we m ust do a " null-che ck" o n
t he m Me diaPlaye r befo re actually re gist e ring t he list e ne rs. We also change st art () to play() and add pause ()
to better fit music player co nventio ns. Inside pause () and st o p() we use Me diaPlaye r.pause () rather than
Me diaPlaye r.st o p() to keep the MediaPlayer prepared fo r further playback instead o f having to re-call
Me diaPlaye r.pre pare () repeatedly when we want to play the audio file again.
We also implement o verrides fo r o nPause and o nRe sum e so that when either the o rientatio n changes o r the
applicatio n pauses, we can sto p the player and restart it if the audio file is already playing. We also implement
o verrides fo r o nSave Inst ance St at e and o nRe st o re Inst ance St at e to reset the MediaPlayer's pro gress in case
the entire MainActivity is recreated. Since the MediaPlayer is so sensitive to its state, we do n't want to make excessive
o r unnecessary metho d calls o n it which might cause us to lo se track o f its state. Therefo re, instead o f starting o r
pausing the MediaPlayer bo th when MainActivity pauses/resumes and when it saves/resto res its state, we use a new
private variable, m WasPlaying to help us track the state.
Handling MediaPlayer Events and UI Updates
Aside fro m the metho ds used to co mmand the player, MediaPlayer also declares several listeners so that yo u can
implement callbacks fo r different player events. Fo r example, yo ur applicatio n can listen fo r when the MediaPlayer
seeks a new playback po sitio n o r when the media file co mpletes playback. In this sectio n, we'll use these callbacks to
add a seek bar and clean up o ur player's behavio r.
Let's get started! Make these changes to act ivit y_m ain.xm l:
CODE TO TYPE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<TextView
android:id="@+id/song_info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginBottom="15dp"
android:textSize="4pt"
android:text="@string/song_01_info" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/start_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_button_label" />
<Button
android:id="@+id/stop_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/stop_button_label" />
</LinearLayout>
</LinearLayout>
Next, make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediaaudio;
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.media.MediaPlayer;
android.media.MediaPlayer.OnCompletionListener;
android.media.MediaPlayer.OnSeekCompleteListener;
android.os.Bundle;
android.os.Handler;
android.view.View;
android.view.View.OnClickListener;
android.widget.Button;
android.widget.SeekBar;
android.widget.SeekBar.OnSeekBarChangeListener;
public class MainActivity extends Activity {
private MediaPlayer mMediaPlayer;
private
private
private
private
private
Button mStartButton;
Button mStopButton;
boolean mWasPlaying
SeekBar mSeekBar;
Handler mHandler = new Handler();
public void play() {
mMediaPlayer.start(); // MediaPlayer is started.
mStartButton.setText(getResources().getString(R.string.pause_button_label));
mHandler.postDelayed(mSeekBarUpdateRunnable, 200);
mStopButton.setEnabled(true);
}
public void pause() {
mMediaPlayer.pause(); // MediaPlayer is paused.
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
public void stop() {
mMediaPlayer.pause(); // MediaPlayer is paused.
mMediaPlayer.seekTo(0);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
private Runnable mSeekBarUpdateRunnable = new Runnable() {
@Override
public void run() {
if (mMediaPlayer != null) {
mSeekBar.setProgress(mMediaPlayer.getCurrentPosition());
if (mMediaPlayer.isPlaying()) {
mHandler.postDelayed(mSeekBarUpdateRunnable, 200);
}
}
}
};
private OnClickListener mStartOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mMediaPlayer.isPlaying()) {
pause();
} else {
play();
}
}
};
private OnClickListener mStopOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
stop();
}
};
private OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener(
) {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (progress != mMediaPlayer.getCurrentPosition()) {
mMediaPlayer.seekTo(progress);
}
}
};
private OnCompletionListener mMediaPlayerCompletionListener = new OnCompletionListene
r() {
@Override
public void onCompletion(MediaPlayer mp) {
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStopButton.setEnabled(false);
}
};
private OnSeekCompleteListener mMediaPlayerSeekCompleteListener = new OnSeekCompleteL
istener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
mSeekBar.setProgress(mp.getCurrentPosition());
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mSeekBar = (SeekBar)findViewById(R.id.seek_bar);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStopButton.setEnabled(false);
mMediaPlayer = MediaPlayer.create(this, R.raw.persephone_by_snowflake); // MediaPla
yer is prepared.
if (mMediaPlayer != null) {
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
mSeekBar.setMax(mMediaPlayer.getDuration());
mSeekBar.setProgress(0);
mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
mMediaPlayer.setOnSeekCompleteListener(mMediaPlayerSeekCompleteListener);
mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener);
} else {
mStartButton.setEnabled(false);
}
}
@Override
protected void onPause() {
super.onPause();
mWasPlaying = mMediaPlayer.isPlaying();
if (mWasPlaying) {
mMediaPlayer.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mWasPlaying) {
play();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isPlaying", mMediaPlayer.isPlaying());
outState.putInt("progress", mMediaPlayer.getCurrentPosition());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mWasPlaying = savedInstanceState.getBoolean("isPlaying");
mMediaPlayer.seekTo(savedInstanceState.getInt("progress"));
if (!mWasPlaying && savedInstanceState.getInt("progress") > 0) {
mStartButton.setText(getResources().getString(R.string.start_button_label));
}
}
}
Save the mo dified files and run the applicatio n. The seek bar no w appears. If yo u play the audio file, the seek bar
sho ws the pro gress. If yo u mo ve the seek bar manually, the playback mo ves to the appro priate lo catio n in the audio
file. If yo u sto p o r pause, so do es the seek bar. If yo u wait until the audio file finishes playing, the Pause butto n
beco mes Play again and the St o p butto n disables. Pressing Play will play the so ng again fro m the beginning.
So let's take a lo o k at the co de we used to implement o ur seek bar:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
...
public void play() {
mMediaPlayer.start(); // MediaPlayer is started.
mStartButton.setText(getResources().getString(R.string.pause_button_label));
mHandler.postDelayed(mSeekBarUpdateRunnable, 200);
mStopButton.setEnabled(true);
}
...
private OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener(
) {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (progress != mMediaPlayer.getCurrentPosition()) {
mMediaPlayer.seekTo(progress);
}
}
};
private OnCompletionListener mMediaPlayerCompletionListener = new OnCompletionListener(
) {
@Override
public void onCompletion(MediaPlayer mp) {
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStopButton.setEnabled(false);
}
};
private OnSeekCompleteListener mMediaPlayerSeekCompleteListener = new OnSeekCompleteL
istener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
mSeekBar.setProgress(mp.getCurrentPosition());
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mStartButton = (Button)findViewById(R.id.start_button);
mStopButton = (Button)findViewById(R.id.stop_button);
mSeekBar = (SeekBar)findViewById(R.id.seek_bar);
mStartButton.setText(getResources().getString(R.string.start_button_label));
mStopButton.setEnabled(false);
mMediaPlayer = MediaPlayer.create(this, R.raw.persephone_by_snowflake); // MediaPla
yer is prepared.
if (mMediaPlayer != null) {
mStartButton.setOnClickListener(mStartOnClickListener);
mStopButton.setOnClickListener(mStopOnClickListener);
mSeekBar.setMax(mMediaPlayer.getDuration());
mSeekBar.setProgress(0);
mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
mMediaPlayer.setOnSeekCompleteListener(mMediaPlayerSeekCompleteListener);
mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener);
} else {
mStartButton.setEnabled(false);
}
}
...
We added a SeekBar to the UI. After the MediaPlayer initializes with the audio file, t he Se e kBar's m axim um is se t
t o t he durat io n o f t he audio f ile . We also added an OnSe e kBarChange List e ne r to the SeekBar. By
implementing o nPro gre ssChange d o n this listener, we can change the playback po sitio n when a user mo ves the
seek bar slider. Ho wever, we first verify in o nPro gre ssChange d, that we actually have to update the playback
po sitio n to match the seek bar. We also implemented an OnSe e kCo m ple t e List e ne r o n the MediaPlayer so that
when we pro grammatically mo ve the playback po sitio n, the seek bar will update as well. We implemente an
OnCo m ple t io nList e ne r fo r the MediaPlayer so that when the audio file co mpletes playback, the co ntro ls update to
reflect that. When a file has co mpleted playback, the MediaPlayer go es into the PlaybackCompleted state. Finally, we
want to update the seek bar as the audio file plays. To acco mplish this, we create a Runnable that will get the current
playback po sitio n and update the seek bar. We init iat e t he Runnable via a Handle r whe ne ve r we st art
playback, and while t he audio f ile is st ill playing, t he Runnable will e xe cut e e ve ry 20 0 m illise co nds t o
co nt inue updat ing t he se e k bar.
Note
When using a Handler-po sted Runnable to update the UI, do n't make the interval between updates to o
small, o r the applicatio n will spend mo st o f its time updating the UI and the MediaPlayer playback will
slo w do wn.
Wrapping Up Audio
Befo re we finish up, let's talk abo ut a few MediaPlayer states that we haven't discussed yet. The first o ccurs after an
erro r o ccurs. Whenever an erro r o ccurs in the MediaPlayer, it go es into the Error state. When an erro r do es o ccur
tho ugh, yo u can implement the o nErro r callback to handle the erro r. Yo u can mo ve the MediaPlayer back to a usable
state by calling the Me diaPlaye r.re se t metho d, which will mo ve the MediaPlayer to the Idle state.
Seco ndly, there is an asynchro no us alternative to Me diaPlaye r.pre pare d, Me diaPlaye r.pre pare Async. There is a
callback named o nPre pare d to no tify the caller when the MediaPlayer is prepared. Between the Initialized state when
the data so urce is set and when this callback is called, the MediaPlayer is in the Preparing state.
Finally, there is the End state. Whenever yo u have finished using a MediaPlayer o bject, a go o d practice is to call
Me diaPlaye r.re le ase , which releases all the reso urces used by the MediaPlayer. This no t o nly frees up memo ry, but
also can reduce battery co nsumptio n. After calling Me diaPlaye r.re le ase , the MediaPlayer enters the End state fro m
which it can no lo nger be used.
Just to summarize, these are the states o f the Me diaPlaye r:
1. Idle
2. Initialized
3. Preparing
4. Prepared
5. Started
6 . Paused
7. PlaybackCo mpleted
8 . Sto pped
9 . End
10 . Ero r
No w that yo u've had an intro ductio n to the MediaPlayer and wo rking with audio files, in the next lesso n we'll start
co vering ho w to play video files. See yo u there!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Media: Video
Lesson Objectives
At the end o f this lesso n, yo u'll be able to :
create a Video View and play an video file.
utilize the Video View co rrectly within the Activity lifecycle, and thro ugh co nfiguratio n changes.
handle Video View events to add additio nal functio nality to yo ur applicatio n.
In the last lesso n, we started wo rking with media: learning abo ut the MediaPlayer and wo rking with audio playback. No w we'll
lo o k at video playback. We'll learn ho w to get video running in yo ur app with the Video View.
Video Playback with a VideoView
Create a new Andro id pro ject as usual, using this criteria:
Name the pro ject Me diaVide o .
Use the package name co m .o re illyscho o l.andro id2.m e diavide o .
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
Mo dify act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
android:gravity="center"
android:padding="20dp" >
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<VideoView
android:id="@+id/video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLinearLayout>
No w let's add so me video to the pro ject. Do wnlo ad the video belo w by right-clicking o n the link and saving the file to
the /re s/raw fo lder:
Do wnlo ad "Co ffee Cup"
Note
The pro ject fo lders are lo cated o n the V drive in the /wo rkspace fo lder; the full path where yo u sho uld
save the image is V:\wo rkspace \Me diaVide o \re s\raw.
No w, make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediavideo;
import
import
import
import
import
android.app.Activity;
android.net.Uri;
android.os.Bundle;
android.view.Menu;
android.widget.VideoView;
public class MainActivity extends Activity {
private VideoView mVideoView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView)findViewById(R.id.video_view);
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + R.raw.coffee_cup));
mVideoView.start();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Save the mo dified files and run yo ur applicatio n.
When yo ur applicatio n starts up, the video lo ads and begins playing immediately. We haven't placed any o ther co ntro ls
o r views yet, so all it do es is play the video . If yo u change the o rientatio n o f the emulato r, the video plays again fro m
the beginning. Let's talk abo ut what we did here.
OBSERVE: /res/layo ut/activity_main.xml
...
<VideoView
android:id="@+id/video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
...
To add a Video View to the XML layo ut, we added the Vide o Vie w tag and specified the layo ut dimensio ns:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
private VideoView mVideoView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mVideoView = (VideoView)findViewById(R.id.video_view);
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + R.raw.angel_of_peace));
mVideoView.start();
}
}
In the MainAct ivit y class, we actually set the so urce fo r the Video View. Yo u can specify the so urce by a String path o r
by a Uri o bject, using either Vide o Vie w.se t Vide o Pat h o r Vide o Vie w.se t Vide o URI. Here we use d a Uri o bje ct
f o r t he raw re so urce t hat we do wnlo ade d pre vio usly.Then we called Vide o Vie w.st art () to play the video . The
video will begin to play again when we change ro tatio n. Since we haven't implemented any state saving/resto ring
behavio r, when the o rientatio n changes o nCre at e will run again, restarting the video .
The Andro id co nventio n fo r embedded reso urce file paths is "andro id.reso urce://[package]/[res id]", where "[package]"
is the applicatio n package name (such as co m.o reillyscho o l.andro id2.mediavideo ) and "[res id]" is the id generated in
the R.java file that identifies the reso urce (such as "R.raw.co ffee_cup").
Note
The video fo rmat we are displaying is H.26 3, which has the .3gp extensio n. When video is played in yo ur
Andro id applicatio n, make sure that it is in a fo rmat suppo rted by Andro id. Fo r a list o f suppo rted fo rmats,
see the Andro id Develo per Do cumentatio n.
Adding a MediaController to a VideoView
No w that video appears in o ur applicatio n, we'll want to give o ur users so me co ntro l o ver the video playback. To do
this, we'll pro vide a MediaCo ntro ller, which co ntains co nventio nal playback co ntro ls: play/pause, fo rward, and rewind.
Let's get started.
Add the fo llo wing changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediavideo;
import
import
import
import
import
android.app.Activity;
android.net.Uri;
android.os.Bundle;
android.widget.MediaController;
android.widget.VideoView;
public class MainActivity extends Activity {
private VideoView mVideoView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView)findViewById(R.id.video_view);
MediaController controller = new MediaController(this);
mVideoView.setMediaController(controller);
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + R.raw.coffee_cup));
mVideoView.start();
}
}
Save the file and run the applicatio n again.
When the applicatio n starts up and the video starts playing, a panel o f co ntro ls appears briefly at the bo tto m o f the
screen and then slides o ut o f view. If yo u click o n the playing video , the co ntro ls appear again. Yo u can use the
co ntro ls to to ggle play/pause o n the video , as well as mo ve fo rward and backward. There is also a seek bar fo r
navigatio n.
Note
Video may be slo w o r cho ppy o n o ur servers. Yo u might want to try these pro grams o n a lo cal machine
to see the full effect.
OBSERVE: MainActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView)findViewById(R.id.video_view);
MediaController controller = new MediaController(this);
mVideoView.setMediaController(controller);
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + R.raw.coffee_cup));
mVideoView.start();
}
}
Here we cre at e a ne w Me diaCo nt ro lle r o bje ct wit h t he curre nt Co nt e xt and then call
Vide o Playe r.se t Me diaCo nt ro lle r wit h t his inst ance .
VideoView Events and Methods
The Video View class pro vides metho ds fo r co ntro lling playback pro grammatically, as well as callbacks fo r different
events that yo u can use to add mo re functio nality to yo ur applicatio n. Let's explo re so me o f these metho ds and
callbacks.
First, we'll grab a co uple mo re video s to add to o ur pro ject. Right-click o n each o f the links belo w and save the files to
the /re s/raw fo lder:
Do wnlo ad "Angel o f Peace"
Do wnlo ad "Windo w Blinds"
Next, make these changes to st rings.xm l:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
<string
<string
<string
name="app_name">MediaVideo</string>
name="action_settings">Settings</string>
name="hello_world">Hello World!</string>
name="loop_label">Loop</string>
name="next_label">Next</string>
name="previous_label">Previous</string>
name="size_text_format">%1$dx%2$d</string>
</resources>
No w make these changes to act ivit y_m ain.xm l:
CODE TO TYPE: /res/layo ut/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/previous_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/previous_label" />
<Button
android:id="@+id/next_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/next_label" />
<CheckBox
android:id="@+id/loop_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loop_label" />
</LinearLayout>
<VideoView
android:id="@+id/video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/video_size_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>
Finally, make these changes to MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.mediavideo;
import
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.media.MediaPlayer;
android.media.MediaPlayer.OnCompletionListener;
android.media.MediaPlayer.OnPreparedListener;
android.net.Uri;
android.os.Bundle;
android.view.View;
android.view.View.OnClickListener;
android.widget.CheckBox;
android.widget.MediaController;
android.widget.TextView;
android.widget.VideoView;
public class MainActivity extends Activity {
final private int[] VIDEO_IDS = new int[] {R.raw.coffee_cup, R.raw.angel_of_peace, R.
raw.window_blinds};
private int mVideoIndex = 0;
private int mLastProgress = 0;
private boolean mWasPlaying = false;
private TextView mSizeText;
private CheckBox mLoopCheckBox;
private VideoView mVideoView;
private OnClickListener mNextOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mVideoIndex = (mVideoIndex + 1) % VIDEO_IDS.length;
loadVideo(mVideoIndex);
}
};
private OnClickListener mPreviousOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mVideoIndex = mVideoIndex == 0? VIDEO_IDS.length - 1 : mVideoIndex - 1;
loadVideo(mVideoIndex);
}
};
private OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (mLoopCheckBox.isChecked()) {
mVideoView.start();
}
}
};
private OnPreparedListener mOnPreparedListener = new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mSizeText.setText(String.format(getResources().getString(R.string.size_text_forma
t), mp.getVideoWidth(), mp.getVideoHeight()));
}
};
private void loadVideo(int index) {
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + VIDEO_IDS[index]));
mVideoView.start();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLoopCheckBox = (CheckBox)findViewById(R.id.loop_checkbox);
mSizeText = (TextView)findViewById(R.id.video_size_text);
findViewById(R.id.next_button).setOnClickListener(mNextOnClickListener);
findViewById(R.id.previous_button).setOnClickListener(mPreviousOnClickListener);
mVideoView = (VideoView)findViewById(R.id.video_view);
MediaController controller = new MediaController(this);
mVideoView.setMediaController(controller);
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().
getPackageName() + "/" + R.raw.coffee_cup));
mVideoView.start();
mVideoView.setOnPreparedListener(mOnPreparedListener);
mVideoView.setOnCompletionListener(mOnCompletionListener);
loadVideo(0);
}
@Override
protected void onPause() {
super.onPause();
if (mVideoView.isPlaying()) {
mVideoView.pause();
mWasPlaying = true;
} else {
mWasPlaying = false;
}
mLastProgress = mVideoView.getCurrentPosition();
}
@Override
protected void onResume() {
super.onResume();
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + VIDEO_IDS[mVideoIndex]));
mVideoView.seekTo(mLastProgress);
if (mWasPlaying)
mVideoView.start();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("videoIndex", mVideoIndex);
outState.putInt("progress", mVideoView.getCurrentPosition());
outState.putBoolean("wasPlaying", mVideoView.isPlaying());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mVideoIndex = savedInstanceState.getInt("videoIndex");
mLastProgress = savedInstanceState.getInt("progress");
mWasPlaying = savedInstanceState.getBoolean("wasPlaying");
}
}
No w save the mo dified files and run the applicatio n:
So let's review what we've do ne.
OBSERVE: activity_main.xml
...
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/previous_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/previous_label" />
<Button
android:id="@+id/next_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/next_label" />
<CheckBox
android:id="@+id/loop_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loop_label" />
</LinearLayout>
<VideoView
android:id="@+id/video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/video_size_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>
First, we made changes to the UI. We added t wo but t o ns so t he use r can m o ve be t we e n dif f e re nt vide o s. We
added a che ck bo x t hat , whe n clicke d, will re play a vide o clip aut o m at ically o nce it re ache s it s e nd; if it is
unchecked, when the video reaches its end, it will simply sto p playing. Video s play as so o n as they have lo aded. We
also added so m e t e xt t hat displays t he size o f t he vide o be ing playe d. If yo u change the emulato r's
o rientatio n o r if yo u go to the ho me screen and return, yo u will no tice that whenever the applicatio n relo ads the UI, the
video remains in the state it was befo re the change.
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
final private int[] VIDEO_IDS = new int[] {R.raw.coffee_cup, R.raw.angel_of_peace, R.
raw.window_blinds};
private int mVideoIndex = 0;
private int mLastProgress = 0;
private boolean mWasPlaying = false;
private TextView mSizeText;
private CheckBox mLoopCheckBox;
private VideoView mVideoView;
private OnClickListener mNextOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mVideoIndex = (mVideoIndex + 1) % VIDEO_IDS.length;
loadVideo(mVideoIndex);
}
};
private OnClickListener mPreviousOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mVideoIndex = mVideoIndex == 0? VIDEO_IDS.length - 1 : mVideoIndex - 1;
loadVideo(mVideoIndex);
}
};
private OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (mLoopCheckBox.isChecked()) {
mVideoView.start();
}
}
};
private OnPreparedListener mOnPreparedListener = new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mSizeText.setText(String.format(getResources().getString(R.string.size_text_forma
t), mp.getVideoWidth(), mp.getVideoHeight()));
}
};
private void loadVideo(int index) {
mVideoView.setVideoURI(Uri.parse("android.resource://" + getApplicationContext().ge
tPackageName() + "/" + VIDEO_IDS[index]));
mVideoView.start();
}
...
}
In the MainAct ivit y, we mo ved the lo gic that started the Video View into its o wn metho d, lo adVide o . We also created
a st at ic array o f t he re so urce IDs f o r t he vide o s t hat t he use r can play. The lo adVide o metho d takes an
index into that array, and lo ads the video with the reso urce ID at that index. Then lo adVide o starts the Vide o Vie w.
We added a co uple o f callbacks fo r Video View events. The OnPre pare dList e ne r fo r the Video View lets us kno w
We added a co uple o f callbacks fo r Video View events. The OnPre pare dList e ne r fo r the Video View lets us kno w
when the Video View is ready to start playback. Once the Video View is prepared, we can lo o k at pro perties o f the video
to be played. In this case, o nce the Video View is prepared, we f ind o ut t he widt h and he ight o f t he vide o and
display that in the UI. The OnCo m ple t io nList e ne r fo r the Video View fires when the video playback is do ne. In o ur
callback, we che ck t o de t e rm ine whe t he r t he use r has che cke d t he " Lo o p" che ck bo x; if they have, we start
the video again.
We also added callbacks fo r when MainAct ivit y pauses/resumes and saves/resto res state. We added class fields
that ho ld the video 's index, the current playback pro gress, and whether the video was actually playing when the activity
was paused. Then we use the values o f tho se fields to make sure that, when MainAct ivit y resumes, the Video View is
o nce again in the state as befo re the pause.
Wrapping UP
In this lesso n, we learned ho w to display video in o ur applicatio n using the Video View co mpo nent. If yo u ever need to
do advanced video playback, such as adding effects to the rendering, o r yo u just want mo re co ntro l o ver the playback,
yo u might want to lo o k into using the MediaPlayer class and a SurfaceView to display video (tho ugh be warned that
this metho d is much mo re co mplex and pro ne to bugs). Video View actually uses a MediaPlayer and a SurfaceView
internally to do its video rendering and takes care o f the mo re co mplex details. If yo u need simple video display,
Video View is pro bably the best way to go .
By no w yo u have a go o d base o n which to build video -enabled applicatio ns. Go o d luck and see yo u next lesso n!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
WebView
Lesson Objectives
At the end o f this lesso n, yo u'll be able to :
add a WebView to an Andro id applicatio n and lo ad a web page.
render custo m HTML inside o f a WebView.
use the We bSe t t ings class to adjust basic WebView settings.
use the We bChro m e Clie nt class to custo mize the behavio r o f the WebView UI.
use the We bVie wClie nt class to custo mize the behavio r o f co ntent rendering inside the WebView.
enable JavaScript o n pages lo aded in the WebView.
In this lesso n we'll co ver the WebView co mpo nent. As yo u may have guessed, a WebView is fo r lo ading HTML co ntent into
yo ur applicatio n, typically (but no t necessarily) fro m the Internet. Let's get started!
Create a new Andro id pro ject with this criteria:
Name the pro ject We bVie w.
Use the package name co m .o re illyscho o l.andro id2.we bvie w.
Uncheck the Cre at e cust o m launche r ico n bo x.
Assign the Andro id2_Le sso ns wo rking set to the pro ject.
WebView Basics
Let's get started with a basic WebView. First, o pen Andro idManif e st .xm l and make these changes:
CODE TO TYPE: Andro idManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.oreillyschool.android2.webview"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
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>
No w, mo dify act ivit y_m ain.xm l as sho wn:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Next, mo dify MainAct ivit y.java as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
android.os.Bundle;
android.app.Activity;
android.view.Menu;
android.webkit.WebView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView)findViewById(R.id.webview);
webview.loadUrl("http://www.oreillyschool.com/");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Finally, o pen st rings.xm l and make these changes:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
name="app_name">WebView</string>
name="action_settings">Settings</string>
name="menu_settings">Settings</string>
name="hello_world">Hello world!</string>
</resources>
No w save all mo dified files and run the applicatio n. Yo u see the O'Reilly Scho o l o f Techno lo gy ho me page under a
title bar with the applicatio n's name "WebView."
No w that o ur applicatio n is running, let's review the co de.
OBSERVE: Andro idManifest.xml
...
<uses-permission android:name="android.permission.INTERNET" />
...
To lo ad a web page inside o f a WebView, first we add a <use s-pe rm issio n> tag with the permissio n name
" andro id.pe rm issio n.INT ERNET " to grant internet access to o ur applicatio n:
OBSERVE: activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Next, we add a We bVie w to o ur applicatio n's layo ut:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView)findViewById(R.id.webview);
webview.loadUrl("http://www.oreillyschool.com/");
}
}
Then, we cre at e a re f e re nce t o t he We bVie w in t he layo ut , and t e ll it t o lo ad t he de sire d sit e by calling
lo adUrl and passing t he sit e 's URL as a st ring. Since We bVie w.lo adUrl is called in the o nCre at e metho d o f
MainAct ivit y, the site begins lo ading as so o n as the applicatio n lo ads in the emulato r.
WebView do esn't just take URLs. We can also pass in HTML directly, and the WebView will render it. Let's try do ing
that; make these changes to MainAct ivit y.java:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import android.os.Bundle;
import android.app.Activity;
import android.webkit.WebView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
webview.loadUrl("http://www.oreillyschool.com/");
String html = "<html><body>"
+ "Hello, world!<br/>"
+ "</body></html>";
webview.loadData(html, "text/html", null);
}
}
Save the file and run the applicatio n. Yo u see the applicatio n title bar and a simple, "Hello , wo rld!" message.
Let's review the change that we just made.
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
String html = "<html><body>"
+ "Hello, world!<br/>"
+ "</body></html>";
webview.loadData(html, "text/html", null);
}
}
Rather than call WebView.lo adUrl, we create a string co ntaining valid HTML and pass this string to
we bVie w.lo adDat a. The seco nd parameter is a st ring co nt aining t he MIME t ype o f t he dat a passe d. The
t hird param e t e r specifies the enco ding o f the data: base64 o r URL enco ding. If we had base6 4 data, we wo uld pass
base6 4. Fo r any o ther value, lo adDat a will treat the data as ASCII data with URL enco ding. Fo r simplicity, we pass in
null.
Using WebSettings
So far we've implemented a simple WebView that lo ads a URL o r HTML. Ho wever, there are many different features we
can access and custo mizatio ns we can make. These features and custo mizatio ns are co ntro lled by a handful o f
Andro id classes. The first o ne that we'll lo o k into is the We bSe t t ings class.
As its name suggests, the We bSe t t ings class co ntains getters and setters fo r a number o f pro perties related to the
way the WebView accesses the Internet and displays co ntent. Let's use the We bSe t t ings class in o ur applicatio n.
Mo dify MainAct ivit y.java again as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.webkit.WebSettings;
android.webkit.WebView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
String html = "<html><body>"
+ "Hello, world!<br/>"
+ "</body></html>";
webview.loadData(html, "text/html", null);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
webview.loadUrl("http://www.oreillyschool.com/");
}
}
No w save the file and run the applicatio n. Our applicatio n lo aded the OST ho me page again. If yo u start scro lling the
page, yo u'll see two butto ns appear in the lo wer-right co rner that allo w yo u to zo o m in and zo o m o ut o f the page.
Let's review what we did.
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
webview.loadUrl("http://www.oreillyschool.com/");
}
}
We use lo adUrl to lo ad the OST ho me page. Then we add two new lines o f co de. First, we grab t he We bSe t t ings
inst ance t hat be lo ngs t o t he We bVie w by calling We bVie w.ge t Se t t ings(). Then we pass t rue t o
We bSe t t ings.se t Built InZ o o m Co nt ro ls t o t urn o n t he zo o m co nt ro ls.
WebSettings has many different pro perties that we can set o r unset to access WebView features.
Note
The WebSettings instance fro m We bVie w.ge t Se t t ings() is tied to the lifecycle o f the WebView. If yo u try
to execute metho ds o n a WebSettings instance when its WebView no lo nger exists, it will result in
exceptio ns and likely crash yo ur applicatio n.
Let's lo o k at ano ther example o f using WebSettings. Make these changes to MainAct ivit y:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.webkit.WebSettings;
android.webkit.WebView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
settings.setBlockNetworkImage(true);
webview.loadUrl("http://www.oreillyschool.com/");
}
}
Save the file and run the applicatio n. Any images that previo usly lo aded with the OST ho me page are no w go ne, and
o nly a blank space remains in their previo us po sitio n.
Let's examine what we just did.
OBSERVE: MainActivity
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
settings.setBlockNetworkImage(true);
webview.loadUrl("http://www.oreillyschool.com/");
}
}
We add a ne w call t o t he We bSe t t ings o bje ct : se t Blo ckNe t wo rkIm age . By passing t rue to this metho d, we
ask the WebView to blo ck do wnlo ading o f any images fro m the netwo rk. To reverse this, we wo uld pass false to the
same metho d.
There are many o ther metho ds o n a We bSe t t ings o bject that allo w yo u to specify behavio r as we have here. Fo r
mo re info rmatio n o n these metho ds and their uses, check o ut the Andro id Develo per Reference.
Next, let's lo o k at ano ther Andro id class that allo ws us to custo mize a WebView: We bChro m e Clie nt .
Using a WebChromeClient
The WebChro meClient class wo rks a bit differently fro m the WebSettings class. First, when we use the WebSettings
class, we grab a reference to an instance attached to a WebView. With the WebChro meClient, we actually create o ur
o wn instance and assign it to the WebView. Also , while the WebSettings allo ws yo u to set particular pro perties, the
WebChro meClient pro vides an interface to respo nd to different events that o ccur in the WebView specific to the UI.
Let's take a lo o k at ho w this wo rks.
Mo dify MainAct ivit y.java as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.view.Window;
android.webkit.WebChromeClient;
android.webkit.WebSettings;
android.webkit.WebView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
settings.setBlockNetworkImage(true);
webview.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
webview.loadUrl("http://www.oreillyschool.com/community/");
}
}
Save the file and run the applicatio n again. Earlier, when yo u ran the applicatio n, but befo re the page lo aded, yo u saw
just a white screen. No w yo u see a pro gress bar under the "WebView" title filling up as the page lo ads and then
disappearing when the page finishes.
Let's lo o k at ho w we did this:
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
webview.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
webview.loadUrl("http://www.oreillyschool.com/community/");
}
}
First, we enable the screen's pro gress indicato r by getting a reference to windo w via ge t Windo w(), then enable the
indicato r by calling Windo w.re que st Fe at ure wit h Windo w.FEAT URE_PROGRESS.
Next, we remo ve the WebSettings.setBlo ckNetwo rkImage call so that we can see images o n the page again. Then we
go back to the WebView and call se t We bChro m e Clie nt . Fo r the se t We bChro m e Clie nt argument, we create an
ano nymo us class that o verrides We bChro m e Clie nt .o nPro gre ssChange d.
Note
Fo r mo re info rmatio n o n ano nymo us classes, see Oracle's Java Do cumentatio n.
Our We bChro m e Clie nt 's o nPro gre ssChange d implementatio n sets MainActivity's pro gre ss indicat o r value
whenever the client receives a pro gress event fro m the lo ading o f the web page. We m ult iply t he pro gre ss variable
passe d t o o nPro gre ssChange d by 10 0 because, while the range fo r o nPro gressChanged is 0 to 10 0 , the range
fo r se t Pro gre ss is 0 to 10 0 0 0 .
We bChro m e Clie nt co ntains o ther callbacks, including callbacks fo r JavaScript events. To see all o f the callbacks
and metho ds available, see the Andro id Develo per Reference.
Befo re we go o n, let's take a clo ser lo o k at o ur applicatio n. If we click o n a link in the page lo aded in the WebView,
instead o f lo ading the link in the WebView, Andro id starts the default bro wser and lo ads the page in that. To fix that and
make so me o ther custo mizatio ns, we'll use the We bVie wClie nt class in the next sectio n.
Using WebViewClient
The We bVie wClie nt class is similar to WebChro meClient in that, to use it, we create a WebViewClient set up with the
desired pro perties and then assign it to a WebView. Ho wever, WebViewClient co ntains callbacks fo r events related to
co ntent rendering: when the page starts lo ading, when the page finishes lo ading, and when there is an erro r in the
lo ading, amo ng o thers. Go ahead and try o ut the WebViewClient.
Mo dify MainAct ivit y.java again as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.view.Window;
android.webkit.WebChromeClient;
android.webkit.WebSettings;
android.webkit.WebView;
android.webkit.WebViewClient;
android.widget.Toast;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
webview.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
webview.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
Toast.makeText(MainActivity.this, "Finished loading: " + url, Toast.LENGTH_SHOR
T).show();
}
});
webview.loadUrl("http://www.oreillyschool.com/community/");
}
}
Save the file and run the applicatio n again. When the page finishes lo ading, the applicatio n displays a to ast message
saying that o ur URL has finished lo ading.
Let's review ho w we used WebViewClient to sho w the to ast message.
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
WebSettings settings = webview.getSettings();
settings.setBuiltInZoomControls(true);
webview.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
webview.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
Toast.makeText(MainActivity.this, "Finished loading: " + url, Toast.LENGTH_SHOR
T).show();
}
});
webview.loadUrl("http://www.oreillyschool.com/");
}
}
Similar to when we intro duced WebChro meClient into o ur applicatio n, we cre at e an ano nym o us class f ro m
We bVie wClie nt and assign t hat t o t he We bVie w by calling We bVie w.se t We bVie wClie nt . Our ano nymo us
WebViewClient class o verrides the o nPage Finishe d metho d. This metho d is a callback fo r when the WebView
co mpletes the lo ading o f a page. Inside this callback, we cre at e and sho w a t o ast m e ssage wit h t he t e xt
" Finishe d lo ading" and t he URL we just lo ade d.
This is just a basic example, but o f co urse, yo u can create mo re so phisticated behavio rs whenever an event o ccurs
during co ntent rendering. Fo r example, the o nRe ce ive dErro r metho d will be called whenever co ntent rendering
generates an erro r and pro vides an erro r co de detailing the issue: a bad URL, failure to co nnect to the server,
authenticatio n pro blem, timeo ut, o r so me o ther issue. Yo u co uld o verride o nRe ce ive dErro r in yo ur WebViewClient
to handle these erro rs, maybe by sho wing a UI where the user can re-enter bad values o r pro mpting them to mo ve to
ano ther part o f the applicatio n.
Finally, if yo u click o n links in the WebView no w, the links no w lo ad in the WebView itself instead o f in the default
bro wser.
WebViewClient co ntains a metho d called sho uldOve rride UrlLo ading. This metho d determines whether the
WebView o r the applicatio n handles the lo ading o f a URL. When there is no WebViewClient o n the WebView, the
applicatio n's Act ivit yManage r decides which to o l sho uld handle the URL (usually the default bro wser). When there is
a WebViewClient assigned to the WebView, We bVie wClie nt .sho uldOve rride UrlLo ading is called to determine
whether the WebView o r the applicatio n handles the URL. The base implementatio n o f this metho d always has the
WebView handle the URL. So we had to assign a WebViewClient to the WebView to have it handle all links.
These are just a few o f the tasks yo u can acco mplish with the WebViewClient. No w we'll go back to the WebView itself
and examine so me handy metho ds fo r navigating its histo ry and implementing a mo re bro wser-like UI.
Using WebView Methods
The WebView class has many metho ds that pro vide functio nality we find in traditio nal bro wsers: bro wsing histo ry,
finding text o n the page, relo ading the page, clearing the cache, and so o n. Let's lo o k at so me examples o f
implementing so me bro wser-style UIs with a WebView.
First, o pen st rings.xm l and make these changes:
CODE TO TYPE: /res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
<string
<string
<string
<string
name="app_name">webview</string>
name="action_settings">Settings</string>
name="menu_settings">Settings</string>
name="back">Back</string>
name="forward">Forward</string>
</resources>
No w o pen act ivit y_m ain.xm l and make these changes:
CODE TO TYPE: /res/layo ut/activity_main.xml
<RelativeLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/controls_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/back_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".5"
android:text="@string/back" />
<Button
android:id="@+id/forward_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".5"
android:text="@string/forward" />
</LinearLayout>
</RelativeLinearLayout>
Finally, mo dify MainAct ivit y.java as sho wn:
CODE TO TYPE: MainActivity.java
package com.oreillyschool.android2.webview;
import
import
import
import
import
import
import
import
import
import
import
android.app.Activity;
android.os.Bundle;
android.view.Window;
android.view.View;
android.view.View.OnClickListener;
android.webkit.WebChromeClient;
android.webkit.WebSettings;
android.webkit.WebView;
android.webkit.WebViewClient;
android.widget.Button;
android.widget.Toast;
public class MainActivity extends Activity {
protected WebView mWebView;
protected Button mBackButton;
protected Button mForwardButton;
private OnClickListener mBackButtonOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mWebView.goBack();
}
};
private OnClickListener mForwardButtonOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mWebView.goForward();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
WebView webview = (WebView) findViewById(R.id.webview);
mWebView = (WebView) findViewById(R.id.webview);
WebSettings settings = webviewmWebView.getSettings();
settings.setBuiltInZoomControls(true);
webviewmWebView.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
webviewmWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
mBackButton.setEnabled(mWebView.canGoBack());
mForwardButton.setEnabled(mWebView.canGoForward());
Toast.makeText(MainActivity.this, "Finished loading: " + url, Toast.LENGTH_
SHORT).show();
}
});
mBackButton = (Button) findViewById(R.id.back_button);
mBackButton.setOnClickListener(mBackButtonOnClickListener);
mBackButton.setEnabled(false);
mForwardButton = (Button) findViewById(R.id.forward_button);
mForwardButton.setOnClickListener(mForwardButtonOnClickListener);
mForwardButton.setEnabled(false);
webviewmWebView.loadUrl("http://www.oreillyschool.com/community/");
}
}
Save all mo dified files and run the applicatio n. Yo u no w see two butto ns belo w the WebView, Back and Fo rward. As
yo u click links and lo ad new pages, yo u can use these butto ns to mo ve backward and fo rward thro ugh yo ur bro wsing
histo ry. These butto ns are enabled o r disabled, depending o n whether yo u are at the beginning, middle, o r end o f yo ur
bro wsing histo ry.
Let's review ho w we implemented this histo ry navigatio n.
OBSERVE: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity" >
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/controls_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/back_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".5"
android:text="@string/back" />
<Button
android:id="@+id/forward_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".5"
android:text="@string/forward" />
</LinearLayout>
</LinearLayout>
First, we add a co uple o f new strings fo r the butto n labels.
Seco nd, we add the Back and Fo rward butto ns to the bo tto m o f the activity layo ut. We also change the We bVie w
layo ut he ight to fill the space abo ve the butto ns.
OBSERVE: MainActivity.java
...
public class MainActivity extends Activity {
protected WebView mWebView;
protected Button mBackButton;
protected Button mForwardButton;
private OnClickListener mBackButtonOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mWebView.goBack();
}
};
private OnClickListener mForwardButtonOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
mWebView.goForward();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webview);
WebSettings settings = mWebView.getSettings();
settings.setBuiltInZoomControls(true);
mWebView.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
mBackButton.setEnabled(mWebView.canGoBack());
mForwardButton.setEnabled(mWebView.canGoForward());
}
});
mBackButton = (Button) findViewById(R.id.back_button);
mBackButton.setOnClickListener(mBackButtonOnClickListener);
mBackButton.setEnabled(false);
mForwardButton = (Button) findViewById(R.id.forward_button);
mForwardButton.setOnClickListener(mForwardButtonOnClickListener);
mForwardButton.setEnabled(false);
mWebView.loadUrl("http://www.oreillyschool.com/community/");
}
}
Finally, we add a re f e re nce t o t he We bVie w and t wo but t o ns in MainAct ivit y. We also add a click list e ne r f o r
e ach but t o n. The click listener fo r the "Back" butto n uses We bVie w.go Back to mo ve backwards thro ugh the
WebView's page histo ry. The click listener fo r the "Fo rward" butto n uses We bVie w.go Fo rward to mo ve fo rward
thro ugh the WebView's page histo ry. We also add lo gic t o We bVie wClie nt .o nPage Finishe d t o e nable o r
disable t he " Back" and " Fo rward" but t o ns, de pe nding o n whe t he r t he use r can m o ve f o rward o r back.
To determine whether the user can mo ve fo rward o r back, we call the We bVie w.canGo Back and
We bVie w.canGo Fo rward metho ds. So , we are able to use the WebView's metho ds in o rder to create navigatio n fo r
the WebView's histo ry via UI co mpo nents.
Befo re we finish with the WebView, we'll swing back to the We bSe t t ings class and talk briefly abo ut JavaScript.
Enabling JavaScript
A WebView can also run JavaScript o n a lo aded page. JavaScript is no t enabled by default, but we can enable it. First,
let's take a lo o k at a page that uses JavaScript. Mo dify the URL in MainAct ivit y.java as sho wn:
CODE TO TYPE: MainActivity.java
...
mWebView.loadUrl("http://www.oreillyschool.com/community/http://www.google.com/logo
s/2013/zamboni.html");
}
}
Save the file and run the applicatio n. Yo u'll see an o ld ho mepage image fo r Go o gle (a Go o gle Do o gle). If yo u're
having tro uble because the image is wide, try ro tating the emulato r to landscape mo de. If yo u viewed this page with
JavaScript enabled, yo u wo uld see a little zambo ni driving acro ss the image, and when yo u clicked the image, it wo uld
link yo u to a JavaScript game o n ano ther page. Ho wever, since o ur WebView do es no t have JavaScript enabled, we
just see a static image link that do es no thing when we click o n it.
Let's enable JavaScript and see what we get. Mo dify MainAct ivit y.java again as sho wn:
CODE TO TYPE: MainActivity.java
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webview);
WebSettings settings = mWebView.getSettings();
settings.setBuiltInZoomControls(true);
settings.setJavaScriptEnabled(true);
mWebView.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
setProgress(progress * 100);
}
});
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
mBackButton.setEnabled(mWebView.canGoBack());
mForwardButton.setEnabled(mWebView.canGoForward());
}
});
mBackButton = (Button) findViewById(R.id.back_button);
mBackButton.setOnClickListener(mBackButtonOnClickListener);
mBackButton.setEnabled(false);
mForwardButton = (Button) findViewById(R.id.forward_button);
mForwardButton.setOnClickListener(mForwardButtonOnClickListener);
mForwardButton.setEnabled(false);
mWebView.loadUrl("http://www.google.com/logos/2013/zamboni.html");
}
}
Save the file and run the applicatio n again. After a little while, yo u see the zambo ni driving acro ss the image. If yo u click
o n the image, yo u will be directed to a new page that sho ws a JavaScript game.
Note
Run a larger emulato r to see the who le game. Depending o n ho w yo u co nfigured yo ur emulated device,
the animatio n may appear to run extremely slo wly.
To enable JavaScript, we call We bSe t t ings.se t J avaScript Enable d(t rue ). That's it!
WebView Wrap-up
As we have seen, the WebView is a fully featured widget fo r displaying HTML co ntent, bo th static and o n the web. While
the WebView itself has many o f the typical features yo u wo uld find in a bro wser like histo ry and text search, so me
custo mizatio n o f a WebView is managed by o ther classes.
The We bSe t t ings class ho lds basic settings fo r the WebView including enabling JavaScript, which allo ws access to
netwo rk reso urces, and default fo nt families and fo nt sizes.
The We bChro m e Clie nt class ho lds callbacks fo r whenever an event o ccurs within the WebView UI. This includes
JavaScript alerts and events and lo ading pro gress events, as well as o ther metho ds related to video lo ading.
The We bVie wClie nt class ho lds callbacks fo r whenever an event o ccurs during co ntent rendering. This includes
when pages start and finish lo ading, when page lo ad erro rs o ccur, o r when there is an authenticatio n/lo gic request.
The WebViewClient also co ntro ls ho w links within a WebView's lo aded page are handled.
By leveraging all o f these classes, yo u can fully utilize web co ntent inside yo ur Andro id applicatio n and even create a
mo re custo m experience o ver the default bro wser.
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.
Android 2 Final Project
Final Project
Co ngratulatio ns o n co mpleting the lesso ns! Fo r the final pro ject yo u will create ano ther Andro id applicatio n. The type
o f applicatio n yo u create is entirely up to yo u, as lo ng as yo u inco rpo rate so me o f the features yo u have learned fro m
the lesso ns in this co urse. Belo w are a few sample ideas that yo u might use fo r yo ur pro ject:
A jo urnal applicatio n. This type o f applicatio n allo ws the user to create jo urnal entries. The entries are sto red
in the applicatio n's Co ntentPro vider, and include the date, a title, the descriptio n, and an o ptio nal pho to .
An RSS reader applicatio n. An RSS feed (such as the OST blo g, o r Stack Overflo w) is co nsumed by a
Service in the applicatio n. The user is no tified when a new feed item appears. Any links in the feed can be
lo aded in a WebView inside the applicatio n.
An updated versio n o f yo ur applicatio n fro m the first Andro id Co urse. Start with whatever yo u made fro m the
first co urse fo r the final pro ject, but update it to use fragments, suppo rt tablets, and include new features
such as Camera, Video Player, a Co ntentPro vider sto rage, o r No tificatio ns.
Whatever final pro ject yo u decide to take o n, yo ur applicatio n must meet these requirements:
Inco rpo rate functio ns o n Andro id devices running Andro id 2.3 and later.
Use Fragments and FragmentActivities.
Has alternate o ptimized layo uts fo r tablets.
All data lo ading is perfo rmed using a Lo ader, such that if the device is ro tated while lo ading the lo ading
pro cess is preserved and no t restarted.
The applicatio n reso urces are used pro perly. That is, all hard-co ded strings are lo aded fro m a strings.xml
reso urce, dimensio ns via a dimens.xml, styles via a styles.xml, and a theme is defined fo r the applicatio n
lo aded via a themes.xml reso urce.
Make an applicatio n that yo u are pro ud to have created! Keep yo ur co de clean, o rganized, and bug-free! Yo u might
even co nsider publishing yo ur wo rk o n the Andro id Market when yo u are finished. Thanks fo r taking the co urse and
go o d luck!
Copyright © 1998-2014 O'Reilly Media, Inc.
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
See http://creativecommons.org/licenses/by-sa/3.0/legalcode for more information.