Android Programming The Big Nerd Ranch Guide
User Manual:
Open the PDF directly: View PDF .
Page Count: 625
Download | ![]() |
Open PDF In Browser | View PDF |
Android Programming THE BIG NERD RANCH GUIDE BILL PHILLIPS & BRIAN HARDY Android Programming: The Big Nerd Ranch Guide by Bill Phillips and Brian Hardy Copyright © 2013 Big Nerd Ranch, Inc. All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, contact Big Nerd Ranch, Inc. 1989 College Ave. Atlanta, GA 30317 (404) 478-9005 http://www.bignerdranch.com/ book-comments@bignerdranch.com The 10-gallon hat with propeller logo is a trademark of Big Nerd Ranch, Inc. Exclusive worldwide distribution of the English edition of this book by Pearson Technology Group 800 East 96th Street Indianapolis, IN 46240 USA http://www.informit.com The authors and publisher have taken care in writing and printing this book but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. Android, Dalvik, Google Play, Google TV, Google Chrome, Chrome, Gmail, and Nexus are trademarks of Google Inc. Eclipse is a trademark of Eclipse Foundation, Inc. Java, and all Java-based trademarks are trademarks of Oracle Corporation and/or its affiliates. Mac is a trademark of Apple Inc., registered in the U.S. and other countries. Windows is a registered trademark of Microsoft Corporation in the United States and other countries. Linux is the registered trademark of Linus Torvalds in the U.S. and other countries. Flickr is a registered trademark of Yahoo! Inc. Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals. ISBN-10 0321804333 ISBN-13 978-0321804334 First edition, second printing, September 2013 Dedication For Donovan. May he live a life filled with activities and know when to use fragments. — B.H. iii This page intentionally left blank Acknowledgments We feel a bit sheepish having our names on the cover of this book. The truth is that without an army of collaborators, this book could never have happened. We owe them all a debt of gratitude. • Chris Stewart and Owen Matthews, who contributed great foundational content for several chapters. • Our co-instructors, Chris Stewart and Christopher Moore. We thank them for their patience in teaching work-in-progress material, their suggestions and corrections for that material, and their consultation when we were considering sweeping changes. • Our coworkers Bolot Kerimbaev and Andrew Lunsford. Their feedback was instrumental in our decision to highlight the use of fragments. • Our technical reviewers, Frank Robles, Jim Steele, Laura Cassell, Mark Dalrymple, and Magnus Dahl, who helped us find and fix flaws. • Thanks to Aaron Hillegass. Aaron’s faith in people is one of the great and terrifying forces of nature. Without it, we would never have had the opportunity to write this book, nor would we ever have completed it. (He also gave us money, which was very friendly of him.) • Our editor, Susan Loper, has an amazing ability to turn our programmery ramblings and bad jokes into thoughtful, concise prose. And better jokes. Without her help, this would not have been a fun book to read. She taught us everything we know about clear and approachable technical writing. • Thanks to NASA. Our little book seems small and silly in comparison to exploring the solar system. • Ellie Volckhausen, who designed our cover. • Elizabeth Holaday, our copy-editor, who found and smoothed rough spots. • Chris Loper at IntelligentEnglish.com, who designed and produced the print book and the EPUB and Kindle versions. His DocBook toolchain made life much easier, too. • The folks at Facebook, who gave us so much fantastic feedback on the course. Finally, thanks to our students. We wish that we had room to thank every single student who gave us a correction or opinion on the book as it was shaping up. It is your curiosity we have worked to satisfy, your confusions we have worked to clarify. Thank you. v This page intentionally left blank Table of Contents Learning Android .......................................................................................................... xvii Prerequisites ......................................................................................................... xvii How to Use This Book .......................................................................................... xvii How This Book Is Organized ................................................................................. xviii Challenges .................................................................................................. xviii Are you more curious? ................................................................................... xix Code Style ............................................................................................................ xix Typographical Conventions ...................................................................................... xix Android Versions .................................................................................................... xx The Necessary Tools ............................................................................................... xx Downloading and installing the ADT Bundle ....................................................... xx Downloading earlier SDK versions .................................................................... xxi A hardware device ........................................................................................ xxii 1. Your First Android Application ....................................................................................... 1 App Basics .............................................................................................................. 2 Creating an Android Project ....................................................................................... 3 Navigating in Eclipse ................................................................................................ 7 Laying Out the User Interface ..................................................................................... 8 The view hierarchy ......................................................................................... 12 Widget attributes ............................................................................................. 12 Creating string resources .................................................................................. 14 Previewing the layout ...................................................................................... 14 From Layout XML to View Objects ........................................................................... 15 Resources and resource IDs .............................................................................. 16 Wiring Up Widgets ................................................................................................. 18 Organizing imports .......................................................................................... 19 Getting references to widgets ............................................................................ 20 Setting listeners .............................................................................................. 20 Running on the Emulator ......................................................................................... 24 For the More Curious: Android Build Process .............................................................. 26 Android build tools ......................................................................................... 28 2. Android and Model-View-Controller ............................................................................... 31 Creating a New Class .............................................................................................. 32 Generating getters and setters ............................................................................ 33 Model-View-Controller and Android .......................................................................... 35 Benefits of MVC ............................................................................................ 36 Updating the View Layer ......................................................................................... 37 Updating the Controller Layer ................................................................................... 39 Running on a Device ............................................................................................... 44 Connecting your device .................................................................................... 44 Configuring your device for development ............................................................ 45 Adding an Icon ...................................................................................................... 45 Adding resources to a project ............................................................................ 46 Referencing resources in XML .......................................................................... 47 Challenges ............................................................................................................. 48 vii Android Programming 3. 4. 5. 6. Challenge: Add a Listener to the TextView .................................................................. 49 Challenge: Add a Previous Button ............................................................................. 49 Challenge: From Button to ImageButton ..................................................................... 49 The Activity Lifecycle ................................................................................................. 53 Logging the Activity Lifecycle .................................................................................. 54 Making log messages ...................................................................................... 54 Using LogCat ................................................................................................. 56 Rotation and the Activity Lifecycle ............................................................................ 61 Device configurations and alternative resources .................................................... 61 Saving Data Across Rotation .................................................................................... 65 Overriding onSaveInstanceState(Bundle) ............................................................. 66 The Activity Lifecycle, Revisited ............................................................................... 67 For the More Curious: Testing onSaveInstanceState(Bundle) ........................................... 69 For the More Curious: Logging Levels and Methods ..................................................... 70 Debugging Android Apps ............................................................................................. 73 The DDMS Perspective ............................................................................................ 74 Exceptions and Stack Traces ..................................................................................... 76 Diagnosing misbehaviors .................................................................................. 77 Logging stack traces ........................................................................................ 78 Setting breakpoints .......................................................................................... 80 Using exception breakpoints ............................................................................. 83 File Explorer .......................................................................................................... 84 Android-Specific Debugging ..................................................................................... 85 Using Android Lint ......................................................................................... 85 Issues with the R class ..................................................................................... 87 Your Second Activity ................................................................................................... 89 Setting Up a Second Activity .................................................................................... 90 Creating a new layout ...................................................................................... 91 Creating a new activity subclass ........................................................................ 95 Declaring activities in the manifest ..................................................................... 97 Adding a Cheat button to QuizActivity ............................................................... 98 Starting an Activity ................................................................................................. 99 Communicating with intents ............................................................................ 100 Passing Data Between Activities .............................................................................. 101 Using intent extras ........................................................................................ 102 Getting a result back from a child activity ......................................................... 104 How Android Sees Your Activities ........................................................................... 109 Challenge ............................................................................................................. 112 Android SDK Versions and Compatibility ...................................................................... 113 Android SDK Versions ........................................................................................... 113 Compatibility and Android Programming ................................................................... 114 Honeycomb was big ...................................................................................... 114 Minimum SDK version .................................................................................. 116 Target SDK version ....................................................................................... 116 Build SDK version ........................................................................................ 116 Adding code from later APIs safely .................................................................. 118 Using the Android Developer Documentation ............................................................. 120 Challenge: Reporting the Build Version ..................................................................... 122 viii Android Programming 7. UI Fragments and the Fragment Manager ....................................................................... The Need for UI Flexibility .................................................................................... Introducing Fragments ............................................................................................ Starting CriminalIntent ........................................................................................... Creating a new project ................................................................................... Fragments and the support library .................................................................... Creating the Crime class ................................................................................. Hosting a UI Fragment ........................................................................................... The fragment lifecycle ................................................................................... Two approaches to hosting .............................................................................. Defining a container view ............................................................................... Creating a UI Fragment .......................................................................................... Defining CrimeFragment’s layout ..................................................................... Creating the CrimeFragment class .................................................................... Adding a UI Fragment to the FragmentManager ......................................................... Fragment transactions .................................................................................... The FragmentManager and the fragment lifecycle ............................................... The Reason All Our Activities Will Use Fragments ..................................................... For the More Curious: Developing for Honeycomb, ICS, Jelly Bean, and Beyond .............. 8. Creating User Interfaces with Layouts and Widgets .......................................................... Upgrading Crime .................................................................................................. Updating the Layout .............................................................................................. Wiring Widgets ..................................................................................................... More on XML Layout Attributes ............................................................................. Styles, themes, and theme attributes .................................................................. Screen pixel densities and dp and sp ................................................................. Android’s design guidelines ............................................................................ Layout parameters ......................................................................................... Margins vs. padding ...................................................................................... Using the Graphical Layout Tool ............................................................................. Adding a new widget ..................................................................................... Editing attributes in properties ......................................................................... Reorganizing widgets in the outline view ........................................................... Updating child layout parameters ..................................................................... How android:layout_weight works .................................................................... Summary of graphical layout tool .................................................................... Widget IDs and multiple layouts ...................................................................... Challenge: Formatting the Date ............................................................................... 9. Displaying Lists with ListFragment ............................................................................... Updating CriminalIntent’s Model Layer ..................................................................... Singletons and centralized data storage .............................................................. Creating a ListFragment ......................................................................................... An Abstract Activity for Hosting a Fragment ............................................................. A generic fragment-hosting layout .................................................................... An abstract Activity class ............................................................................... ListFragment, ListView, and ArrayAdapter ................................................................ Creating an ArrayAdapter......................................................................... Responding to list item clicks .......................................................................... 125 126 126 127 130 131 133 134 134 135 136 137 137 139 142 143 145 147 147 149 149 150 152 153 153 154 155 156 156 157 159 160 161 162 164 165 165 166 167 168 169 170 171 172 172 178 180 183 ix Android Programming 10. 11. 12. 13. 14. 15. x Customizing List Items .......................................................................................... Creating the list item layout ............................................................................ Creating an adapter subclass ........................................................................... Using Fragment Arguments ....................................................................................... Starting an Activity from a Fragment ........................................................................ Putting an extra ............................................................................................ Retrieving an extra ........................................................................................ Updating CrimeFragment’s view with Crime data ................................................ The downside to direct retrieval ....................................................................... Fragment Arguments .............................................................................................. Attaching arguments to a fragment ................................................................... Retrieving arguments ..................................................................................... Reloading the List ................................................................................................. Getting Results with Fragments ............................................................................... Using ViewPager ..................................................................................................... Creating CrimePagerActivity ................................................................................... Laying out views in code ................................................................................ ViewPager and PagerAdapter ........................................................................... Integrating CrimePagerActivity ........................................................................ FragmentStatePagerAdapter vs. FragmentPagerAdapter ........................................ For the More Curious: How ViewPager Really Works .................................................. Dialogs .................................................................................................................. Creating a DialogFragment ..................................................................................... Showing a DialogFragment ............................................................................. Setting a dialog’s contents .............................................................................. Passing Data Between Two Fragments ...................................................................... Passing data to DatePickerFragment ................................................................. Returning data to CrimeFragment ..................................................................... Challenge: More Dialogs ........................................................................................ Audio Playback Using MediaPlayer ............................................................................ Adding Resources ................................................................................................. Defining the layout for HelloMoonFragment .............................................................. Manually resetting the app theme ..................................................................... Creating HelloMoonFragment .................................................................................. Using a Layout Fragment ....................................................................................... Audio Playback .................................................................................................... Wiring buttons to play and stop ....................................................................... Challenge: Pausing Audio Playback .......................................................................... For the More Curious: Playing Video ........................................................................ Challenge: Playing Video in HelloMoon .................................................................... Retained Fragments .................................................................................................. Retaining a Fragment ............................................................................................. Rotation and Retained Fragments ............................................................................. Retained Fragments: As Nice as All That? ................................................................. Rotation Handling and onSaveInstanceState(Bundle) .................................................... For the More Curious: Rotation Before Fragments ...................................................... Localization ............................................................................................................ Localizing Resources ............................................................................................. 184 184 187 191 191 192 193 193 195 195 195 196 196 198 201 202 203 204 205 208 209 211 213 214 215 217 218 220 225 227 228 230 231 231 232 233 235 236 236 236 237 237 238 240 241 243 245 245 Android Programming 16. 17. 18. 19. Default resources ........................................................................................... Configuration Qualifiers ......................................................................................... Prioritizing alternative resources ....................................................................... Multiple qualifiers ......................................................................................... Finding the best-matching resources ................................................................. More Resource Rules and Regulations ...................................................................... Resource naming ........................................................................................... Resource directory structure ............................................................................ Testing Alternative Resources .................................................................................. The Action Bar ........................................................................................................ Options Menus ..................................................................................................... Defining an options menu in XML ................................................................... Creating the options menu .............................................................................. Responding to options menu selections ............................................................. Enabling Ancestral Navigation ................................................................................. Enabling the app icon .................................................................................... Responding to the Up button ........................................................................... An Alternative Menu Item ...................................................................................... Creating an alternative menu file ...................................................................... Toggling the menu item title ........................................................................... “Just one more thing...” .................................................................................. Challenge: An Empty View for the List ..................................................................... Saving and Loading Local Files .................................................................................. Saving and Loading Data in CriminalIntent ................................................................ Saving crime data to a JSON file ..................................................................... Loading crimes from the filesystem .................................................................. Challenge: Use External Storage .............................................................................. For the More Curious: The Android Filesystem and Java I/O ......................................... Accessing files and directories ......................................................................... Context Menus and Contextual Action Mode ................................................................ Defining a Context Menu Resource .......................................................................... Implementing a Floating Context Menu ..................................................................... Creating the context menu .............................................................................. Registering for the context menu ...................................................................... Responding to an action ................................................................................. Implementing Contextual Action Mode ..................................................................... Enabling multiple selection ............................................................................. Action mode callbacks in a list view ................................................................. Changing activated item backgrounds ............................................................... Implementing contextual action mode in other views ........................................... Compatibility: Fallback or Duplicate? ....................................................................... Challenge: Deleting from CrimeFragment .................................................................. For the More Curious: ActionBarSherlock ................................................................. Challenge: Using ActionBarSherlock ........................................................................ Basic ABS integration in CriminalIntent ............................................................ More advanced integration .............................................................................. Even more advanced integration ....................................................................... Camera I: Viewfinder ............................................................................................... 246 247 247 249 250 251 251 251 251 253 253 255 257 260 262 262 264 266 266 267 268 270 273 273 274 278 280 280 281 283 284 284 284 285 286 287 288 289 291 293 293 294 294 296 297 297 298 299 xi Android Programming 20. 21. 22. 23. xii Creating the Fragment Layout ................................................................................. Creating CrimeCameraFragment .............................................................................. Creating CrimeCameraActivity ................................................................................ Adding activity and camera permissions to the manifest ....................................... Using the Camera API ........................................................................................... Opening and releasing the camera .................................................................... SurfaceView, SurfaceHolder, and Surface ........................................................... Determining preview size ............................................................................... Starting CrimeCameraActivity from CrimeFragment ............................................ For the More Curious: Running Activities from the Command Line ................................ Camera II: Taking Pictures and Handling Images ........................................................... Taking a Picture .................................................................................................... Implementing Camera callbacks ....................................................................... Setting the picture size ................................................................................... Passing Data Back to CrimeFragment ....................................................................... Starting CrimeCameraActivity for a result ......................................................... Setting a result in CrimeCameraFragment .......................................................... Retrieving filename in CrimeFragment .............................................................. Updating the Model Layer ...................................................................................... Adding a Photo class ..................................................................................... Giving Crime a photo property ........................................................................ Setting the photo property ............................................................................... Updating CrimeFragment’s View .............................................................................. Adding an ImageView .................................................................................. Image handling ............................................................................................. Showing Larger Image in a DialogFragment .............................................................. Challenge: Crime Image Orientation ......................................................................... Challenge: Deleting Photos ..................................................................................... For the More Curious: Deprecation in Android ........................................................... Implicit Intents ........................................................................................................ Adding Buttons ..................................................................................................... Adding a Suspect to the Model Layer ....................................................................... Using a Format String ............................................................................................ Using Implicit Intents ............................................................................................ Parts of an implicit intent ............................................................................... Sending a crime report ................................................................................... Asking Android for a contact .......................................................................... Checking for responding activities .................................................................... Challenge: Another Implicit Intent ........................................................................... Two-Pane Master-Detail Interfaces .............................................................................. Adding Layout Flexibility ....................................................................................... Modifying SingleFragmentActivity ................................................................... Creating a layout with two fragment containers ................................................... Using an alias resource .................................................................................. Activity: Fragment Boss ......................................................................................... Fragment callback interfaces ........................................................................... For the More Curious: More on Determining Device Size ............................................. More About Intents and Tasks .................................................................................... 300 302 303 303 304 304 306 310 311 316 319 319 321 324 324 325 326 327 328 329 330 330 331 332 334 338 341 341 342 345 346 348 348 350 350 351 353 357 357 359 360 361 361 363 365 365 373 375 Android Programming 24. 25. 26. 27. 28. Setting Up NerdLauncher ....................................................................................... Resolving an Implicit Intent .................................................................................... Creating Explicit Intents at Runtime ......................................................................... Tasks and the Back Stack ....................................................................................... Using NerdLauncher as a Home Screen .................................................................... Challenge: Icons, Reordering Tasks .......................................................................... For the More Curious: Processes vs. Tasks ................................................................. Styles And Includes ................................................................................................. Setting Up the RemoteControl Project ....................................................................... Setting up RemoteControlActivity .................................................................... Setting up RemoteControlFragment .................................................................. Cleaning Up with Styles ......................................................................................... Finishing the Layout .............................................................................................. For the More Curious: include and merge .................................................................. Challenge: Style Inheritance .................................................................................... XML Drawables And 9-Patches .................................................................................. XML Drawables ................................................................................................... State List Drawables .............................................................................................. Layer List and Inset Drawables ................................................................................ Using 9-Patch Images ............................................................................................ HTTP & Background Tasks ....................................................................................... Creating PhotoGallery ............................................................................................ Networking Basics ................................................................................................ Asking permission to network ......................................................................... Using AsyncTask to Run on a Background Thread ...................................................... You and Your Main Thread ..................................................................................... Beyond the main thread .................................................................................. Fetching XML From Flickr ..................................................................................... Using XmlPullParser ...................................................................................... From AsyncTask Back to the Main Thread ................................................................ For the More Curious: More on AsyncTask ................................................................ Cleaning Up AsyncTasks ................................................................................ Challenge: Paging ................................................................................................. Loopers, Handlers, and HandlerThread ........................................................................ Preparing GridView for Displaying Images ................................................................ Downloading Lots of Small Things .......................................................................... Communicating with the Main Thread ...................................................................... Assembling a Background Thread ............................................................................ Messages and Message Handlers .............................................................................. Message anatomy .......................................................................................... Handler anatomy ........................................................................................... Using handlers .............................................................................................. Passing handlers ............................................................................................ For the More Curious: AsyncTask vs. Threads ............................................................ Challenge: Preloading and Caching .......................................................................... Search .................................................................................................................... Searching Flickr .................................................................................................... The Search Dialog ................................................................................................. 375 376 379 380 383 384 384 387 388 388 389 392 394 397 398 399 400 402 404 406 413 414 417 419 419 421 422 422 426 429 431 432 433 435 435 438 438 439 441 441 441 442 445 449 450 451 451 453 xiii Android Programming 29. 30. 31. 32. xiv Creating a search interface .............................................................................. Searchable activities ....................................................................................... Hardware search button .................................................................................. How Search works ........................................................................................ Launch modes and new intents ........................................................................ Simple persistence with shared preferences ........................................................ Using SearchView on Post-Android 3.0 ..................................................................... Challenges ........................................................................................................... Background Services ................................................................................................ Creating an IntentService ........................................................................................ What Services Are For ........................................................................................... Safe background networking ........................................................................... Looking for New Results ........................................................................................ Delayed Execution with AlarmManager .................................................................... PendingIntent ............................................................................................... Managing alarms with PendingIntent ................................................................ Controlling Your Alarm .......................................................................................... Updating options menu items .......................................................................... Notifications ......................................................................................................... For the More Curious: Service Details ...................................................................... What service does (and does not) do ................................................................. A service’s lifecycle ...................................................................................... Non-sticky services ........................................................................................ Sticky services .............................................................................................. Bound services ............................................................................................. Broadcast Intents ..................................................................................................... Waking Up on Boot ............................................................................................... Broadcast receivers in the manifest ................................................................... How to use receivers ...................................................................................... Filtering Foreground Notifications ............................................................................ Sending broadcast intents ............................................................................... Dynamic broadcast receivers ........................................................................... Using private permissions ............................................................................... Receiving results with ordered broadcasts .......................................................... Receivers and Long-running Tasks ........................................................................... Browsing The Web & WebView ................................................................................. One Last Bit of Flickr Data .................................................................................... The Easy Way: Implicit Intents ................................................................................ The Harder Way: WebView ..................................................................................... Using WebChromeClient to spruce things up ...................................................... Proper rotation with WebView ......................................................................... For the More Curious: Injecting JavaScript Objects ..................................................... Custom Views and Touch Events ................................................................................ Setting Up the DragAndDraw Project ....................................................................... Setting up DragAndDrawActivity ..................................................................... Setting up DragAndDrawFragment ................................................................... Creating a Custom View ......................................................................................... Creating BoxDrawingView .............................................................................. 453 455 458 458 459 461 463 465 467 467 470 470 471 472 474 475 475 477 478 480 480 480 481 481 481 485 485 486 488 489 489 490 492 495 499 501 501 503 503 508 509 510 513 513 514 515 516 516 Android Programming Handling Touch Events .......................................................................................... Tracking across motion events ......................................................................... Rendering Inside onDraw(…) .................................................................................. Challenge: Rotations .............................................................................................. 33. Tracking the Device’s Location .................................................................................. Getting Started with RunTracker ............................................................................. Setting up RunActivity ................................................................................... Setting up RunFragment ................................................................................. Locations and the LocationManager ......................................................................... Receiving Broadcast Location Updates ...................................................................... Updating the UI with Location Data ......................................................................... Faster Answers: the Last Known Location ................................................................. Testing Locations on Real and Virtual Devices ........................................................... 34. Local Databases with SQLite ..................................................................................... Storing Runs and Locations in a Database ................................................................. Querying a List of Runs From the Database ............................................................... Displaying a List of Runs Using CursorAdapter .......................................................... Creating New Runs ............................................................................................... Working with Existing Runs .................................................................................... Challenge: Identifying the Current Run ..................................................................... 35. Loading Asynchronous Data With Loaders ................................................................... Loaders and the LoaderManager .............................................................................. Using Loaders in RunTracker .................................................................................. Loading the List of Runs ........................................................................................ Loading a Single Run ............................................................................................ Loading the Last Location for a Run ........................................................................ 36. Using Maps ............................................................................................................ Adding the Maps API to RunTracker ........................................................................ Use a real device to test maps ......................................................................... Install and use the Google Play services SDK ..................................................... Obtain a Google Maps API key ....................................................................... Update RunTracker’s manifest ......................................................................... Showing the User’s Location on a Map ..................................................................... Displaying a Run’s Path ......................................................................................... Adding Markers for Run Start and Finish .................................................................. Challenge: Live Updates ......................................................................................... 37. Afterword ............................................................................................................... The Final Challenge .............................................................................................. Shameless Plugs .................................................................................................... Thank You ........................................................................................................... Index ........................................................................................................................... 518 519 522 524 525 525 526 527 528 531 532 536 537 541 541 548 550 553 555 560 561 561 563 563 567 570 573 573 573 573 574 574 575 579 582 583 585 585 585 585 587 xv This page intentionally left blank Learning Android As a beginning Android programmer, you face a steep learning curve. Learning Android is like moving to a foreign city. Even if you speak the language, it will not feel like home at first. Everyone around you seems to understand things that you are missing. Things you already knew turn out to be dead wrong in this new context. Android has a culture. That culture speaks Java, but knowing Java is not enough. Getting your head around Android requires learning many new ideas and techniques. It helps to have a guide through unfamiliar territory. That’s where we come in. At Big Nerd Ranch, we believe that to be an Android programmer, you must: • write Android applications • understand what you are writing This guide will help you do both. We have trained hundreds of professional Android programmers using it. We lead you through writing several Android applications, introducing concepts and techniques as needed. When there are rough spots, when some things are tricky or obscure, you will face it head on, and we will do our best to explain why things are the way they are. This approach allows you to put what you have learned into practice in a working app right away rather than learning a lot of theory and then having to figure out how to apply it all later. You will come away with the experience and understanding you need to get going as an Android developer. Prerequisites To use this book, you need to be familiar with Java, including classes and objects, interfaces, listeners, packages, inner classes, anonymous inner classes, and generic classes. If these ideas do not ring a bell, you will be in the weeds by page 2. Start instead with an introductory Java book and return to this book afterward. There are many excellent introductory books available, so you can choose one based on your programming experience and learning style. If you are comfortable with object-oriented programming concepts, but your Java is a little rusty, you will probably be okay. We will provide some brief reminders about Java specifics (like interfaces and anonymous inner classes). Keep a Java reference handy in case you need more support as you go through the book. How to Use This Book This book is not a reference book. Its goal is to get you over the initial hump to where you can get the most out of the reference and recipe books available. It is based on our five-day class at Big Nerd Ranch. As such, it is meant to be worked through from the beginning. Chapters build on each other and skipping around is unproductive. In our classes, students work through these materials, but they also benefit from the right environment – a dedicated classroom, good food and comfortable board, a group of motivated peers, and an instructor to answer questions. xvii Learning Android As a reader, you want your environment to be similar. That means getting a good night’s rest and finding a quiet place to work. These things can help, too: • Start a reading group with your friends or coworkers. • Arrange to have blocks of focused time to work on chapters. • Participate in the forum for this book at forums.bignerdranch.com. • Find someone who knows Android to help you out. How This Book Is Organized In this book, you will write eight Android apps. A couple are very simple and take only a chapter to create. Others are more complex. The longest app spans thirteen chapters. All are designed to teach you important concepts and techniques and give you direct experience using them. GeoQuiz In your first app, you will explore the fundamentals of Android projects, activities, layouts, and explicit intents. CriminalIntent The largest app in the book, CriminalIntent lets you keep a record of your colleagues’ lapses around the office. You will learn to use fragments, masterdetail interfaces, list-backed interfaces, menus, the camera, implicit intents, and more. HelloMoon In this small shrine to the Apollo program, you will learn more about fragments, media playback, resources, and localization. NerdLauncher Building this custom launcher will give you insight into the intent system and tasks. RemoteControl In this toy app, you will learn to use styles, state list drawables, and other tools to create attractive user interfaces. PhotoGallery A Flickr client that downloads and displays photos from Flickr’s public feed, this app will take you through services, multithreading, accessing web services, and more. DragAndDraw In this simple drawing app, you will learn about handling touch events and creating custom views. RunTracker This app lets you track and display on a map your travels around town (or around the world). In it, you will learn how to use location services, SQLite databases, loaders, and maps. Challenges Most chapters have a section at the end with exercises for you to work through. This is your opportunity to use what you have learned, explore the documentation, and do some problem-solving on your own. xviii Are you more curious? We strongly recommend that you do the challenges. Going off the beaten path and finding your way will solidify your learning and give you confidence with your own projects. If you get lost, you can always visit forums.bignerdranch.com for some assistance. Are you more curious? There are also sections at the ends of chapters labeled “For the More Curious.” These sections offer deeper explanations or additional information about topics presented in the chapter. The information in these sections is not absolutely essential, but we hope you will find it interesting and useful. Code Style There are three areas where our choices differ from what you might see elsewhere in the Android community: We use anonymous inner classes for listeners. This is mostly a matter of opinion. We find it makes for cleaner code. It puts the listener’s method implementations right where you want to see them. In high performance contexts, anonymous inner classes may cause problems, but for most circumstances they work fine. After we introduce fragments in Chapter 7, we use them for all user interfaces. This is something we feel strongly about. Many Android developers still write activity-based code. We would like to challenge that practice. Once you get comfortable with fragments, they are not that difficult to work with. Fragments have clear advantages over activities that make them worth the effort, including flexibility in building and presenting your user interfaces. We write apps to be compatible with Gingerbread and Froyo devices. The Android platform has changed with the introduction of Ice Cream Sandwich and Jelly Bean and soon Key Lime Pie. However, the truth is that half of devices in use still run Froyo or Gingerbread. (You will learn about the different and deliciously-named Android versions in Chapter 6.) Therefore, we intentionally take you through the difficulties involved in writing apps that are backwards-compatible with Froyo or at least Gingerbread. It is easier to learn, teach, and program in Android if you start with the latest platform. But we want you to be able to develop in the real world where Gingerbread phones still make up more than 40% of devices. Typographical Conventions To make this book easier to read, certain items appear in certain fonts. Variables, constants, and types appear in a fixed-width font. Class names, interface names, and method names appear in a bold, fixedwidth font. xix Learning Android All code and XML listings will be in a fixed-width font. Code or XML that you need to type in is always bold. Code or XML that should be deleted is struck through. For example, in the following method implementation, you are deleting the call to makeText(…) and adding the call to checkAnswer(true). @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.incorrect_toast, Toast.LENGTH_SHORT).show(); checkAnswer(true); } Android Versions This book teaches Android development for all widely-used versions of Android. As of this writing, that is Android 2.2 (Froyo) - Android 4.2 (Jelly Bean). As Android releases new versions, we will keep track of changes at forums.bignerdranch.com and offer notes on using this book with the latest version. The Necessary Tools To get started, you will need the ADT (Android Developer Tools) Bundle. This includes: Eclipse an integrated development environment used for Android development. Because Eclipse is also written in Java, you can install it on a PC, a Mac, or a Linux computer. The Eclipse user interface follows the “native look-and-feel” of your machine, so your screen may not look exactly like screenshots in this book. Android Developer Tools a plug-in for Eclipse. This book uses ADT (Android Developer Tools) 21.1. You should make sure you have that version or higher. Android SDK the latest version of the Android SDK Android SDK tools and platform-tools tools for debugging and testing your apps A system image for the Android emulator lets you create and test your apps on different virtual devices Downloading and installing the ADT Bundle The ADT Bundle is available from Android’s developer site as a single zip file. 1. Download the bundle from http://developer.android.com/sdk/index.html. xx Downloading earlier SDK versions 2. Extract the zip file to where you want Eclipse and the other tools installed. 3. In the extracted files, find and open the eclipse directory and launch Eclipse. If you are running on Windows, and Eclipse will not start, you may need to install the Java Development Kit (JDK6), which you can download from www.oracle.com. If you are still having problems, return to http://developer.android.com/sdk/index.html for more information. Downloading earlier SDK versions The ADT Bundle provides the SDK and the emulator system image from the latest platform. However, you will need other platforms to test your apps on earlier versions of Android. You can get components for each platform using the Android SDK Manager. In Eclipse, select Window → Android�SDK�Manager. Figure 1 Android SDK Manager For every version going back to Android 2.2 (Froyo), we recommend selecting and installing: • the SDK�Platform • an�emulator�system�image • the Google�APIs Note that downloading these components may take a while. xxi Learning Android The Android SDK Manager is also how to get Android’s latest releases, like a new platform or an update of the tools. A hardware device The emulator is useful for testing apps. However, it is good to have an actual Android device to run apps on as well. The last app in the book will require a hardware device. xxii 1 Your First Android Application This first chapter is full of new concepts and moving parts required to build an Android application. It is OK if you do not understand everything by the end of this chapter. You will be revisiting these ideas again and in greater detail as you proceed through the book. The application you are going to create is called GeoQuiz. GeoQuiz tests the user’s knowledge of geography. The user presses True or False to answer the question on screen, and GeoQuiz provides instant feedback. Figure 1.1 shows the result of a user pressing the False button: Figure 1.1 (It’s Istanbul, not Constantinople) 1 Chapter 1 Your First Android Application App Basics Your GeoQuiz application will consist of an activity and a layout: • An activity is an instance of Activity, a class in the Android SDK. An activity is responsible for managing user interaction with a screen of information. You write subclasses of Activity to implement the functionality that your app requires. A simple application may need only one subclass; a complex application can have many. GeoQuiz is a simple app, so it will have a single Activity subclass named QuizActivity. QuizActivity will manage the user interface shown in Figure 1.1. • A layout defines a set of user interface objects and their position on the screen. A layout is made up of definitions written in XML. Each definition is used to create an object that appears on screen, like a button or some text. GeoQuiz will include a layout file named activity_quiz.xml. The XML in this file will define the user interface shown in Figure 1.1. The relationship between QuizActivity and activity_quiz.xml is diagrammed in Figure 1.2. Figure 1.2 QuizActivity manages what activity_quiz.xml defines With those ideas in mind, let’s build an app. 2 Creating an Android Project Creating an Android Project The first step is to create an Android project. An Android project contains the files that make up an application. To create a new project, open Eclipse and choose File → New → Android�Application Project to open the new application wizard. In the first dialog, enter GeoQuiz as the application name (Figure 1.3). The project name will automatically update to match the application’s. For the package name, enter com.bignerdranch.android.geoquiz. Figure 1.3 Creating a new application Notice that the package name you entered uses a “reverse DNS” convention in which the domain name of your organization is reversed and suffixed with further identifiers. This convention keeps package names unique and distinguishes applications from each other on a device and on Google Play. The next four fields configure your application to work with different versions of Android. The default settings are what you need for GeoQuiz, so you can ignore these for now. You will learn about the different versions of Android in Chapter 6. Android updates its tools several times a year, so your wizard may look slightly different from what we are showing you. This is usually not a problem; the choices to make should stay pretty much the same. (If your wizard looks very different, then the tools have changed more drastically. Do not panic. Head to this book’s forum at forums.bignerdranch.com, and we will help you navigate the latest version.) Click Next. 3 Chapter 1 Your First Android Application In the second dialog, uncheck the box to create a custom launcher icon (Figure 1.4). GeoQuiz will use the default launcher icon. Be sure to leave the Create�activity box checked. Figure 1.4 Configuring the project Click Next. 4 Creating an Android Project This dialog (Figure 1.5) asks you what kind of activity to create. Choose Blank�Activity. Figure 1.5 Creating a new activity Click Next. 5 Chapter 1 Your First Android Application In the final dialog of this wizard, name the activity subclass QuizActivity (Figure 1.6). Notice the Activity suffix on the class name. This is not required, but it is an excellent convention to follow. Figure 1.6 Configuring the new activity The layout name will automatically update to activity_quiz to reflect the activity’s new name. The layout name reverses the order of the activity name, is all lowercase, and has underscores between words. This naming style is recommended for layouts as well as other resources that you will learn about later. Leave Navigation�Type as None and click Finish. Eclipse will create and open your new project. 6 Navigating in Eclipse Navigating in Eclipse Eclipse opens your project in the workbench window, as shown in Figure 1.7. (If you have a brand-new installation, close Eclipse’s welcome screen to reveal the workbench window.) The different panes of the workbench window are called views. The lefthand view is the package explorer. From the package explorer, you can manage the files associated with your project. The middle view is the editor. To get you started, Eclipse has opened activity_quiz.xml in the editor. There are also views on the righthand side and the bottom of the workbench. Close any views on the righthand side by clicking the x next to the view’s name (Figure 1.7). The views at the bottom are in a tab group. Instead of closing these views, minimize the entire tab group using the control at the group’s top-right corner. Figure 1.7 Uncluttering the workbench window When you minimize a view, it is corralled into a toolbar on the margin of the Eclipse workbench. You can mouse over any of the small icons in these toolbars to see the names of the views and click on any icon to restore that view. 7 Chapter 1 Your First Android Application Laying Out the User Interface By default, Eclipse opened activity_quiz.xml in Android’s graphical layout tool, which shows you a preview of the layout. The graphical layout tool can be useful, but for now you are going to work in XML to get a better understanding of how layouts work. To get to the raw XML, select the tab at the bottom of the editor labeled activity_quiz.xml. Currently, activity_quiz.xml defines the default activity layout. The defaults change frequently, but the XML will look something like Listing 1.1. Listing 1.1 Default activity layout (activity_quiz.xml) First, notice that activity_quiz.xml does not start with a line declaring its version and encoding: As of ADT 21, this line is no longer required in Android layout files. However, you will still see it in many cases. The default activity layout defines two widgets: a RelativeLayout and a TextView. Widgets are the building blocks you use to compose a user interface. A widget can show text or graphics, interact with the user, or arrange other widgets on the screen. Buttons, text input controls, and check boxes are all types of widgets. The Android SDK includes many widgets that you can configure to get the appearance and behavior you want. Every widget is an instance of the View class or one of its subclasses (such as TextView or Button). Figure 1.8 shows how the RelativeLayout and TextView defined in Listing 1.1 would appear on screen. 8 Laying Out the User Interface Figure 1.8 Default widgets as seen on screen But these are not the widgets you are looking for. The interface for QuizActivity requires five widgets: • a vertical LinearLayout • a TextView • a horizontal LinearLayout • two Buttons Figure 1.9 shows how these widgets compose QuizActivity’s interface. 9 Chapter 1 Your First Android Application Figure 1.9 Planned widgets as seen on screen Now you need to define these widgets in activity_quiz.xml. In activity_quiz.xml, make the changes shown in Listing 1.2. The XML that you need to delete is struck through, and the XML that you need to add is in a bold font. This is the pattern we will use throughout the book. Do not worry about understanding what you are typing; you will learn how it works next. However, do be careful. Layout XML is not validated, and typos will cause problems sooner or later. Depending on your version of the tools, you might get errors on the three lines that start with android:text. Ignore these errors for now; you will fix them soon. 10 Laying Out the User Interface Listing 1.2 Defining widgets in XML (activity_quiz.xml) Compare your XML with the user interface shown in Figure 1.9. Every widget has a corresponding XML element. The name of the element is the type of the widget. Each element has a set of XML attributes. Each attribute is an instruction about how the widget should be configured. To understand how the elements and attributes work, it helps to look at the layout from a hierarchical perspective. 11 Chapter 1 Your First Android Application The view hierarchy Your widgets exist in a hierarchy of View objects called the view hierarchy. Figure 1.10 shows the view hierarchy that corresponds to the XML in Listing 1.2. Figure 1.10 Hierarchical layout of widgets and attributes The root element of this layout’s view hierarchy is a LinearLayout. As the root element, the LinearLayout must specify the Android resource XML namespace at http://schemas.android.com/ apk/res/android. inherits from a subclass of View named ViewGroup. A ViewGroup is a widget that contains and arranges other widgets. You use a LinearLayout when you want widgets arranged in a single column or row. Other ViewGroup subclasses are FrameLayout, TableLayout, and RelativeLayout. LinearLayout When a widget is contained by a ViewGroup, that widget is said to be a child of the ViewGroup. The root LinearLayout has two children: a TextView and another LinearLayout. The child LinearLayout has two Button children of its own. Widget attributes Let’s go over some of the attributes that you have used to configure your widgets: 12 Widget attributes android:layout_width and android:layout_height The android:layout_width and android:layout_height attributes are required for almost every type of widget. They are typically set to either match_parent or wrap_content: match_parent view will be as big as its parent wrap_content view will be as big as its contents require (You may see fill_parent in some places. This deprecated value is equivalent to match_parent.) For the root LinearLayout, the value of both the height and width attributes is match_parent. The LinearLayout is the root element, but it still has a parent – the view that Android provides for your app’s view hierarchy to live in. The other widgets in your layout have their widths and heights set to wrap_content. You can see in Figure 1.9 how this determines their sizes. The TextView is slightly larger than the text it contains due to its android:padding="24dp" attribute. This attribute tells the widget to add the specified amount of space to its contents when determining its size. You are using it to get a little breathing room between the question and the buttons. (Wondering about the dp units? These are density-independent pixels that you will learn about in Chapter 8.) android:orientation The android:orientation attribute on the two LinearLayout widgets determines whether their children will appear vertically or horizontally. The root LinearLayout is vertical; its child LinearLayout is horizontal. The order in which children are defined determines the order in which they appear on screen. In a vertical LinearLayout, the first child defined will appear topmost. In a horizontal LinearLayout, the first child defined will be leftmost. (Unless the language of the device is a language that runs right-toleft, such as Arabic or Hebrew. In that case, the first child will be rightmost.) android:text The TextView and Button widgets have android:text attributes. This attribute tells the widget what text to display. Notice that the values of these attributes are not literal strings. They are references to string resources. A string resource is a string that lives in a separate XML file called a strings file. You can give a widget a hard-coded string, like android:text="True", but it is usually not a good idea. Placing strings into a separate file and then referencing them is better. In Chapter 15, you will see how using string resources makes localization easy. The string resources you are referencing in activity_quiz.xml do not exist yet. Let’s fix that. 13 Chapter 1 Your First Android Application Creating string resources Every project includes a default strings file named strings.xml. In the package explorer, find the res/values directory, reveal its contents, and open strings.xml. Ignore the graphical interface and select the strings.xml tab at the bottom of the editor. The template has already added a few string resources for you. Remove the unused string named hello_world and add the three new strings that your layout requires. Listing 1.3 Adding string resources (strings.xml) (Do not delete the menu_settings string. Your project came with a menu already prepared. Deleting menu_settings will cause cascading errors in other files related to the menu.) Now whenever you refer to @string/false_button in any XML file in the GeoQuiz project, you will get the literal string “False” at runtime. Save strings.xml. If you had errors in activity_quiz.xml about the missing string resources, they should now be gone. (If you still have errors, check both files for typos.) The default strings file is named strings.xml, but you can name a strings file anything you want. You can also have multiple strings files in a project. As long as the file is located in res/values/, has a resources root element, and contains child string elements, your strings will be found and used appropriately. Previewing the layout Your layout is now complete, and you can preview the layout in the graphical layout tool. First, make sure that your files are saved and error-free. Then return to activity_quiz.xml and select the Graphical�Layout tab at the bottom of the editor. 14 From Layout XML to View Objects Figure 1.11 Preview in graphical layout tool (activity_quiz.xml) From Layout XML to View Objects How do XML elements in activity_quiz.xml become View objects? The answer starts in the QuizActivity class. When you created the GeoQuiz project, a subclass of Activity named QuizActivity was created for you. The class file for QuizActivity is in the src directory of your project. The src directory is where the Java code for your project lives. In the package explorer, reveal the contents of the src directory and then the contents of the com.bignerdranch.android.geoquiz package. Open the QuizActivity.java file and take a look at its contents (Listing 1.4). 15 Chapter 1 Your First Android Application Listing 1.4 Default class file for QuizActivity (QuizActivity.java) package com.bignerdranch.android.geoquiz; import android.app.Activity; import android.os.Bundle; import android.view.Menu; public class QuizActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_quiz, menu); return true; } } (If you are not seeing all of the import statements, click the ⊕ symbol to the left of the first import statement to reveal the others.) This file has two Activity methods: onCreate(Bundle) and onCreateOptionsMenu(Menu). Ignore onCreateOptionsMenu(Menu) for now. You will return to menus in detail in Chapter 16. The onCreate(Bundle) method is called when an instance of the activity subclass is created. When an activity is created, it needs a user interface to manage. To get the activity its user interface, you call the following Activity method: public void setContentView(int layoutResID) This method inflates a layout and puts it on screen. When a layout is inflated, each widget in the layout file is instantiated as defined by its attributes. You specify which layout to inflate by passing in the layout’s resource ID. Resources and resource IDs A layout is a resource. A resource is a piece of your application that is not code – things like image files, audio files, and XML files. Resources for your project live in a subdirectory of the res directory. In the package explorer, you can see that activity_quiz.xml lives in res/layout/. Your strings file, which contains string resources, lives in res/values/. To access a resource in code, you use its resource ID. The resource ID for your layout is R.layout.activity_quiz. To see the current resource IDs for GeoQuiz, go to the package explorer and reveal the contents of the gen directory. Find and open R.java. Because this file is generated by the Android build process, you should not change it, as you are subtly warned at the top of the file. 16 Resources and resource IDs Listing 1.5 Current GeoQuiz resource IDs (R.java) /* AUTO-GENERATED FILE. ... */ DO NOT MODIFY. package com.bignerdranch.android.geoquiz; public final class R { public static final class attr { } public static final class drawable { public static final int ic_launcher=0x7f020000; } public static final class id { public static final int menu_settings=0x7f070003; } public static final class layout { public static final int activity_quiz=0x7f030000; } public static final class menu { public static final int activity_quiz=0x7f060000; } public static final class string { public static final int app_name=0x7f040000; public static final int false_button=0x7f040003; public static final int menu_settings=0x7f040006; public static final int question_text=0x7f040001; public static final int true_button=0x7f040002; } } ... This is where the R.layout.activity_quiz comes from – it is an integer constant named activity_quiz within the layout inner class of R. Your strings also have resource IDs. You have not yet referred to a string in code, but if you did, it would look like this: setTitle(R.string.app_name); Android generated a resource ID for the entire layout and for each string, but it did not generate IDs for the individual widgets in activity_quiz.xml. Not every widget needs a resource ID. In this chapter, you will only interact with the two buttons in code, so only they need resource IDs. To generate a resource ID for a widget, you include an android:id attribute in the widget’s definition. In activity_quiz.xml, add an android:id attribute to each button. 17 Chapter 1 Your First Android Application Listing 1.6 Adding IDs to buttons (activity_quiz.xml) GeoQuiz name="hello_world">Hello, world! name="question_text">Constantinople is the largest city in Turkey. name="true_button">True name="false_button">False name="menu_settings">SettingsNotice that there is a + sign in the values for android:id but not in the values for android:text. This is because you are creating the IDs and only referencing the strings. Save activity_quiz.xml. Return to R.java to confirm that you have two new resource IDs in the R.id inner class. Listing 1.7 New resource IDs (R.java) public final class R { ... public static final class id { public static final int false_button=0x7f070001; public static final int menu_settings=0x7f070002; public static final int true_button=0x7f070000; } ... Wiring Up Widgets Now that the buttons have resource IDs, you can access them in QuizActivity. The first step is to add two member variables. 18 Organizing imports Type the following code into QuizActivity.java. (Do not use auto-complete; type it in yourself.) After you save the file, it will report two errors. Listing 1.8 Adding member variables (QuizActivity.java) public class QuizActivity extends Activity { private Button mTrueButton; private Button mFalseButton; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); } ... } You will fix the errors in just a second. First, notice the m prefix on the two member (instance) variable names. This prefix is an Android naming convention that we will follow throughout this book. Now mouse over the error flags to the left of the code. They report the same problem: Button�cannot�be resolved�to�a�type. These errors are telling you that you need to import the android.widget.Button class into QuizActivity.java. You could type the following import statement at the top of the file: import android.widget.Button; Or you can do it the easy way and organize your imports. Organizing imports When you organize imports, you ask Eclipse to look at your code and determine what you need from the Java and Android SDKs. It imports what you need and removes any previously imported classes that you are no longer using. To organize your imports, press: • Command+Shift+O for Mac • Ctrl+Shift+O for Windows and Linux This should get rid of the errors. (If you still have errors, check for typos in your code and XML.) Now you can wire up your button widgets. This is a two-step process: • get references to the inflated View objects • set listeners on those objects to respond to user actions 19 Chapter 1 Your First Android Application Getting references to widgets In an activity, you can get a reference to an inflated widget by calling the following Activity method: public View findViewById(int id) This method accepts a resource ID of a widget and returns a View object. In QuizActivity.java, use the resource IDs of your buttons to retrieve the inflated objects and assign them to your member variables. Note that you must cast the returned View to Button before assigning it. Listing 1.9 Getting references to widgets (QuizActivity.java) public class QuizActivity extends Activity { private Button mTrueButton; private Button mFalseButton; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); } mTrueButton = (Button)findViewById(R.id.true_button); mFalseButton = (Button)findViewById(R.id.false_button); ... } Setting listeners Android applications are typically event-driven. Unlike command-line programs or scripts, eventdriven applications start and then wait for an event, such as the user pressing a button. (Events can also be initiated by the OS or another application, but user-initiated events are the most obvious.) When your application is waiting for a specific event, we say that it is “listening for” that event. The object that you create to respond to an event is called a listener. A listener is an object that implements a listener interface for that event. The Android SDK comes with listener interfaces for various events, so you do not have to write your own. In this case, the event you want to listen for is a button “click,” so your listener will implement the View.OnClickListener interface. Start with the True button. In QuizActivity.java, add the following code to onCreate(…) just after the variable assignment. 20 Setting listeners Listing 1.10 Set listener for True button (QuizActivity.java) ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); mTrueButton = (Button)findViewById(R.id.true_button); mTrueButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Does nothing yet, but soon! } }); } } mFalseButton = (Button)findViewById(R.id.false_button); (If you have a View�cannot�be�resolved�to�a�type error, organize your imports using Command+Shift+O or Ctrl+Shift+O to import the View class.) In Listing 1.10, you set a listener to inform you when the Button known as mTrueButton has been pressed. The setOnClickListener(OnClickListener) method takes a listener as its argument. In particular, it takes an object that implements OnClickListener. Using anonymous inner classes This listener is implemented as an anonymous inner class. The syntax is a little tricky, but it helps to remember that everything within the outermost set of parentheses is passed into setOnClickListener(OnClickListener). Within these parentheses, you create a new, nameless class and pass its entire implementation. mTrueButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Does nothing yet, but soon! } }); All of the listeners in this book will be implemented as anonymous inner classes. Doing so puts the implementations of the listener’s methods right where you want to see them. And there is no need for the overhead of a named class because the class will be used in one place only. Because your anonymous class implements OnClickListener, it must implement that interface’s sole method, onClick(View). You have left the implementation of onClick(View) empty for now, and the compiler is okay with that. A listener interface requires you to implement onClick(View), but it makes no rules about how to implement it. (If your knowledge of anonymous inner classes, listeners, or interfaces is rusty, you may want to review some Java before continuing or at least keep a reference nearby.) Set a similar listener for the False button. 21 Chapter 1 Your First Android Application Listing 1.11 Set listener for False button (QuizActivity.java) ... mTrueButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Does nothing yet, but soon! } }); } mFalseButton = (Button)findViewById(R.id.false_button); mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Does nothing yet, but soon! } }); Making toasts Now to make the buttons fully-armed and operational. You are going to have a click of each button trigger a pop-up message called a toast. A toast is a short message that informs the user of something but does not require any input or action. You are going to make toasts that announce whether the user answered correctly or incorrectly (Figure 1.12). Figure 1.12 A toast providing feedback First, return to strings.xml and add the string resources that your toasts will display. 22 Setting listeners Listing 1.12 Adding toast strings (strings.xml) To create a toast, you call the following method from the Toast class: public static Toast makeText(Context context, int resId, int duration) The Context parameter is typically an instance of Activity (Activity is a subclass of Context). The second parameter is the resource ID of the string that the toast should display. The Context is needed by the Toast class to be able to find and use the string’s resource ID. The third parameter is usually one of two Toast constants that specify how long the toast should be visible. After you have created a toast, you call Toast.show() on it to get it on screen. In QuizActivity, you are going to call makeText(…) in each button’s listener (Listing 1.13). Instead of typing everything in, try using Eclipse’s auto-complete feature to add these calls. Using auto-complete Auto-complete can save you a lot of time, so it is good to become familiar with it early. Start typing the code addition shown in Listing 1.13. When you get to the period after the Toast class, a pop-up window will appear with a list of suggested methods and constants from the Toast class. To choose one of the suggestions, press the tab key to focus on the auto-complete pop-up. (If you wanted to ignore auto-complete, you could just keep typing. It will not complete anything for you if you do not press the tab key or click on the pop-up window.) From the list of suggestions, select makeText(Context, int, int). Auto-complete will add the complete method call for you, including placeholder values for the arguments. The first placeholder will be highlighted; type in the real value – QuizActivity.this. Then press the tab key again to go to the next placeholder, and so on until you have added the code shown in Listing 1.13. 23 Chapter 1 Your First Android Application Listing 1.13 Making toasts (QuizActivity.java) ... mTrueButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.incorrect_toast, Toast.LENGTH_SHORT).show(); } }); mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.correct_toast, Toast.LENGTH_SHORT).show(); } }); In makeText(…), you pass the instance of QuizActivity as the Context argument. However, you cannot simply pass the variable this as you might expect. At this point in the code, you are defining the anonymous class where this refers to the View.OnClickListener. Because you used auto-complete, you do not have to organize your imports to get the Toast class. When you accept an auto-complete suggestion, the necessary classes are imported automatically. Running on the Emulator To run an Android application, you need a device – either a hardware device or a virtual device. Virtual devices are powered by the Android emulator, which ships with the developer tools. To create an Android virtual device (AVD), choose Window → Android�Virtual�Device�Manager. When the AVD Manager appears, click the New... button on the righthand side of the window. In the dialog that appears, you are offered many options for configuring a virtual device. For your first AVD, choose to emulate a Galaxy�Nexus device running Google�APIs�–�API�Level�17, as shown in Figure 1.13. If you are running on Windows, you will need to change the RAM from 1024 to 512 to get the AVD to behave properly. Then click OK. 24 Running on the Emulator Figure 1.13 Creating a new AVD Once you have an AVD, you can run GeoQuiz on it. In the package explorer, right-click the GeoQuiz project folder. From the context menu, choose Run�As → Android�Application. Eclipse will find the virtual device you created, install the application package on it, and run the app. Eclipse may ask you if you want to auto-monitor with LogCat. Say yes. Starting up the emulator can take a while, but eventually your GeoQuiz app will launch on the AVD that you created. Press buttons and admire your toasts. (Note that if the app launches and you are not around, you may have to unlock the AVD when you come back. The AVD works like a real device, and it will lock itself after a time.) If GeoQuiz crashes when launching or when you press a button, LogCat will appear at the bottom of your workbench. Look for exceptions in the log; they will be an eye-catching red color. The Text column will tell you the exception’s name and give you the line where the problem occurred. 25 Chapter 1 Your First Android Application Figure 1.14 An example NullPointerException at line 21 Compare your code with the code in the book to try to find the cause of the problem. Then try running again. Keep the emulator running; you do not want to wait for it to launch on every run. You can stop the app by pressing the Back button (the arrow that is making a U-turn). Then re-run the app from Eclipse to test changes. The emulator is useful, but testing on a real device gives more accurate results. In Chapter 2, you will run GeoQuiz on a hardware device. You will also give GeoQuiz more geography questions with which to test the user. For the More Curious: Android Build Process By now, you probably have some burning questions about how the Android build process works. You have already seen that Eclipse builds your project automatically as you modify it rather than on command. During the build process, the Android tools take your resources, code, and the AndroidManifest.xml file (which contains meta-data about the application) and turn them into an .apk file. This file is then signed with a debug key, which allows it to run on the emulator. (To distribute your .apk to the masses, you have to sign it with a release key. There is more information about this process in the Android developer documentation at http://developer.android.com/ tools/publishing/preparing.html.) 26 For the More Curious: Android Build Process Figure 1.15 Building GeoQuiz How do the contents of activity_quiz.xml turn into View objects in an application? As part of the build process, aapt (Android Asset Packaging Tool) compiles layout file resources into a more compact format. These compiled resources are packaged into the .apk file. Then, when setContentView(…) is called in the QuizActivity’s onCreate(…) method, the QuizActivity uses the LayoutInflater class to instantiate each of the View objects as defined in the layout file. 27 Chapter 1 Your First Android Application Figure 1.16 Inflating activity_quiz.xml (You can also create your view classes programmatically in the activity instead of defining them in XML. But there are benefits to separating your presentation from the logic of the application. The main one is taking advantage of configuration changes built into the SDK, which we’ll talk more about in Chapter 3.) For more details on how the different XML attributes work and how views display themselves on the screen, see Chapter 8. Android build tools All of the builds you have seen so far have been executed from within Eclipse. This build is integrated into the ADT plugin you are using – it invokes standard Android build tools like aapt, but the build process itself is managed by Eclipse. You may, for your own reasons, want to perform builds from outside of Eclipse. The easiest way to do this is to use a command line build tool. The most popular two tools are maven and ant. Ant does less, but it is much easier to use. Start by doing two things: • Make sure that ant is installed and that you can run it. • Make sure that both the tools/ and platform-tools/ folders in your Android SDK are included in your executable search paths. Now navigate to your project’s directory and run the following command: $ android update project -p . 28 Android build tools The Eclipse project generator template does not include an appropriate build.xml for ant. This first command generates build.xml for you. You only need to run it this one time. Next, build your project. To build and sign a debug .apk, run the following command in the same folder: $ ant debug This will actually perform your build. It will create an .apk file located in bin/your-project-namedebug.apk. Once you have the .apk in hand, install it by running this command: $ adb install bin/your-project-name-debug.apk This will install your app on whatever device is connected. It will not run the app, though. For that, you will need to pull up the launcher and launch the app by hand. 29 This page intentionally left blank 2 Android and Model-ViewController In this chapter, you are going to upgrade the GeoQuiz app to present more than one question. Figure 2.1 More questions! To make this happen, you are going to add a class named TrueFalse to the GeoQuiz project. An instance of this class will encapsulate a single true-false question. Then, you will create an array of TrueFalse objects for QuizActivity to manage. 31 Chapter 2 Android and Model-View-Controller Creating a New Class In the package explorer, right-click the com.bignerdranch.android.geoquiz package and select New → Class. Name the class TrueFalse, leave its superclass as java.lang.Object, and click Finish. Figure 2.2 Creating the TrueFalse class In TrueFalse.java, add two member variables and a constructor: Listing 2.1 Adding to TrueFalse class (TrueFalse.java) public class TrueFalse { private int mQuestion; private boolean mTrueQuestion; } 32 public TrueFalse(int question, boolean trueQuestion) { mQuestion = question; mTrueQuestion = trueQuestion; } Generating getters and setters Why is mQuestion an int and not a String? The mQuestion variable will hold the resource ID (always an int) of a string resource for the question. You will create the question string resources in a later section. The mTrueQuestion variable indicates whether the statement is true or false. These variables need getter and setter methods. Rather than typing them in yourself, you can have Eclipse generate the implementations for you. Generating getters and setters The first step is to configure Eclipse to recognize the m prefix for member variables and to use is rather than get for boolean variables. Open Eclipse’s preferences (from the Eclipse menu on Mac and from Windows → Preferences on Windows). Under Java preferences, select Code�Style. In the Conventions�for�variable�names: table, select the Fields row (Figure 2.3). Click the Edit button and add m as a prefix for fields. Then add s as a prefix for static fields. (You will not be using the s prefix in the GeoQuiz project, but it will be useful in later projects.) Make sure the Use�‘is’�prefix�for�getters�that�return�boolean is checked. Then click OK. Figure 2.3 Setting Java code style preferences What is the point of setting these prefixes? Now when you ask Eclipse to generate a getter for mQuestion, it will create getQuestion() rather than getMQuestion() and isTrueQuestion() rather than isMTrueQuestion(). 33 Chapter 2 Android and Model-View-Controller Back in TrueFalse.java, right-click after the constructor and select Source → Generate�Getters�And Setters... Click the Select�All button to create a getter and setter for each variable. Click OK, and Eclipse will generate code for the four methods. Listing 2.2 Generated getters and setters (TrueFalse.java) public class TrueFalse { private int mQuestion; private boolean mTrueQuestion; public TrueFalse(int question, boolean trueQuestion) { mQuestion = question; mTrueQuestion = trueQuestion; } public int getQuestion() { return mQuestion; } public void setQuestion(int question) { mQuestion = question; } public boolean isTrueQuestion() { return mTrueQuestion; } public void setTrueQuestion(boolean trueQuestion) { mTrueQuestion = trueQuestion; } } Your TrueFalse class is now complete. In a moment, you will modify QuizActivity to work with TrueFalse. First, let’s take a look at the big picture of how the pieces of GeoQuiz will work together. You are going to have QuizActivity create an array of TrueFalse objects. It will then interact with the TextView and the three Buttons to display questions and provide feedback on the user’s responses. 34 Model-View-Controller and Android Figure 2.4 Object diagram for GeoQuiz Model-View-Controller and Android Notice that the objects in Figure 2.4 are separated into three sections labeled Model, Controller, and View. Android applications are designed around an architecture called Model-View-Controller, or MVC for short. MVC states that any object in your application must be a model object, a view object, or a controller object. • A model object holds the application’s data and “business logic.” Model classes are typically designed to model the things your app is concerned with, such as a user, a product in a store, a photo on a server, or a television show. Or a true-false question. Model objects have no knowledge of the user interface; their sole purpose is holding and managing data. In Android applications, model classes are generally custom classes you create. All of the model objects in your application compose its model layer. GeoQuiz’s model layer consists of the TrueFalse class. • View objects know how to draw themselves on the screen and how to respond to user input, like touches. A simple rule of thumb is that if you can see it on screen, then it is a view. Android provides a wealth of configurable view classes. You can also create custom view classes. An application’s view objects make up its view layer. GeoQuiz’s view layer consists of the widgets that are inflated from activity_quiz.xml. 35 Chapter 2 Android and Model-View-Controller • Controller objects tie the view and model objects together. They contain “application logic.” Controllers are designed to respond to various events triggered by view objects and to manage the flow of data to and from model objects and the view layer. In Android, a controller is typically a subclass of Activity, Fragment, or Service. (You will learn about fragments in Chapter 7 and services in Chapter 29.) GeoQuiz’s controller layer, at present, consists solely of QuizActivity. Figure 2.5 shows the flow of control between objects in response to a user event, like a press of a button. Notice that model and view objects do not talk to each other directly; controllers sit squarely in the middle of everything, receiving messages from some objects and dispatching instructions to others. Figure 2.5 MVC flow with user input Benefits of MVC An application can accumulate features until it is too complicated to understand. Separating code into classes helps you design and understand the application as a whole; you can think in terms of classes instead of individual variables and methods. Similarly, separating classes into model, view, and controller layers helps you design and understand an application; you can think in terms of layers instead of individual classes. GeoQuiz is not a complicated app, but you can still see the benefits of keeping layers separate here. In a moment, you are going to update GeoQuiz’s view layer to include a Next button. When you do that, you will not need to remember a single thing about the TrueFalse class you just created. MVC also makes classes easier to reuse. A class with restricted responsibilities is more reusable than one with its fingers in every pie. For instance, your model class, TrueFalse, knows nothing about the widgets used to display a truefalse question. This makes it easy to use TrueFalse throughout your app for different purposes. For 36 Updating the View Layer example, if you wanted to display a list of all the questions at once, you could use the same object that you use here to display just one question at a time. Updating the View Layer Now that you have been introduced to MVC, let’s update GeoQuiz’s view layer to include a Next button. In Android, objects in the view layer are typically inflated from XML within a layout file. The sole layout in GeoQuiz is defined in activity_quiz.xml. This layout needs updating as shown in Figure 2.6. (Note that we are not showing the attributes of unchanged widgets to save space.) Figure 2.6 New button! So the changes you need to make to the view layer are • remove the android:text attribute from the TextView. You no longer want a hard-coded question to be part of its definition. • Give the TextView an android:id attribute. This widget will need a resource ID so that you can set its text in QuizActivity’s code. • Add the new Button widget as a child of the root LinearLayout. Return to activity_quiz.xml and make it happen. 37 Chapter 2 Android and Model-View-Controller Listing 2.3 New button... and changes to the text view (activity_quiz.xml) GeoQuiz Constantinople is the largest city in Turkey. True False Correct! Incorrect! Settings Save activity_quiz.xml, and you may see a familiar error pop up to alert you about a missing string resource. Return to res/values/strings.xml. Remove the question string and add a string for the new button. Listing 2.4 Updating strings (strings.xml) ... ... GeoQuiz name="question_text">Constantinople is the largest city in Turkey. name="true_button">True name="false_button">False name="next_button">Next name="correct_toast">Correct! ... While you have strings.xml open, go ahead and add the strings for the set of geography questions that will be shown to the user. 38 Updating the Controller Layer Listing 2.5 Adding question strings in advance (strings.xml) ...Incorrect! Settings The Pacific Ocean is larger than the Atlantic Ocean. The Suez Canal connects the Red Sea and the Indian Ocean. The source of the Nile River is in Egypt. The Amazon River is the longest river in the Americas. Lake Baikal is the world\'s oldest and deepest freshwater lake. ... Notice that you use the escape sequence \' in the last value to get an apostrophe in your string. You can use all the usual escape sequences in your string resources, such as \n for a new line. Save your files. Then return to activity_quiz.xml and preview your layout changes in the graphical layout tool. That is all for now for GeoQuiz’s view layer. Time to wire everything up in your controller class, QuizActivity. Updating the Controller Layer In the previous chapter, there was not that much happening in GeoQuiz’s one controller, QuizActivity. It displayed the layout defined in activity_quiz.xml. It set listeners on two buttons and wired them to make toasts. Now that you have multiple questions to retrieve and display, QuizActivity will have to work harder to tie GeoQuiz’s model and view layers together. Open QuizActivity.java. Add variables for the TextView and the new Button. Also, create an array of TrueFalse objects and an index for the array. Listing 2.6 Adding variables and a TrueFalse array (QuizActivity.java) public class QuizActivity extends Activity { private private private private Button mTrueButton; Button mFalseButton; Button mNextButton; TextView mQuestionTextView; private new new new new new }; TrueFalse[] mQuestionBank = new TrueFalse[] { TrueFalse(R.string.question_oceans, true), TrueFalse(R.string.question_mideast, false), TrueFalse(R.string.question_africa, false), TrueFalse(R.string.question_americas, true), TrueFalse(R.string.question_asia, true), private int mCurrentIndex = 0; ... 39 Chapter 2 Android and Model-View-Controller Here you call the TrueFalse constructor several times and create an array of TrueFalse objects. (In a more complex project, this array would be created and stored elsewhere. In later apps, you will see better options for storing model data. For now, we are keeping it simple and just creating the array within your controller.) You are going to use mQuestionBank, mCurrentIndex, and the accessor methods in TrueFalse to get a parade of questions on screen. First, get a reference for the TextView and set its text to the question at the current index. Listing 2.7 Wiring up the TextView (QuizActivity.java) public class QuizActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } } mTrueButton = (Button)findViewById(R.id.true_button); ... Save your files and check for any errors. Then run GeoQuiz. You should see the first question in the array appear in the TextView. Now let’s see about the Next button. First, get a reference to the button. Then set a View.OnClickListener on it. This listener will increment the index and update the TextView’s text. 40 Updating the Controller Layer Listing 2.8 Wiring up the new button (QuizActivity.java) public class QuizActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); ... mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.correct_toast, Toast.LENGTH_SHORT).show(); } }); } } mNextButton = (Button)findViewById(R.id.next_button); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } }); You now have code in two separate places that updates the mQuestionTextView variable. Take a moment to put this code into a private method instead, as shown in Listing 2.9. Then call that method in the mNextButton’s listener and at the end of onCreate(Bundle) to initially set the text in the activity’s view. 41 Chapter 2 Android and Model-View-Controller Listing 2.9 Encapsulating with updateQuestion() (QuizActivity.java) public class QuizActivity extends Activity { ... private void updateQuestion() { int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } @Override protected void onCreate(Bundle savedInstanceState) { ... mQuestionTextView = (TextView)findViewById(R.id.question_text_view); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); updateQuestion(); } }); } } updateQuestion(); Run GeoQuiz and test your new Next button. Now that you have the questions behaving appropriately, it is time to turn to the answers. Here again, you will implement a private method to encapsulate code rather than writing similar code in two places. The method that you are going to add to QuizActivity is private void checkAnswer(boolean userPressedTrue) This method will accept a boolean variable that identifies whether the user pressed True or False. Then, it will check the user’s answer against the answer in the current TrueFalse object. Finally, after determining whether the user answered correctly, it will make a Toast that displays the appropriate message to the user. In QuizActivity.java, add the implementation of checkAnswer(boolean) shown in Listing 2.10. 42 Updating the Controller Layer Listing 2.10 Adding checkAnswer(boolean) (QuizActivity.java) public class QuizActivity extends Activity { ... private void updateQuestion() { int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } private void checkAnswer(boolean userPressedTrue) { boolean answerIsTrue = mQuestionBank[mCurrentIndex].isTrueQuestion(); int messageResId = 0; if (userPressedTrue == answerIsTrue) { messageResId = R.string.correct_toast; } else { messageResId = R.string.incorrect_toast; } } Toast.makeText(this, messageResId, Toast.LENGTH_SHORT) .show(); @Override protected void onCreate(Bundle savedInstanceState) { ... } } Within the button’s listeners, call checkAnswer(boolean), as shown in Listing 2.11. 43 Chapter 2 Android and Model-View-Controller Listing 2.11 Calling checkAnswer(boolean) (QuizActivity.java) public class QuizActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... mTrueButton = (Button)findViewById(R.id.true_button); mTrueButton.setOnClickListener(new View.OnClickListener() { }); @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.incorrect_toast, Toast.LENGTH_SHORT).show(); checkAnswer(true); } mFalseButton = (Button)findViewById(R.id.false_button); mFalseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(QuizActivity.this, R.string.correct_toast, Toast.LENGTH_SHORT).show(); checkAnswer(false); } }); mNextButton = (Button)findViewById(R.id.next_button); ... } } GeoQuiz is ready to run again. Let’s get it running on a real device. Running on a Device In this section, you will set up your system, device, and application to get GeoQuiz running on your hardware device. Connecting your device First, plug the device into your system. If you are developing on a Mac, your system should recognize the device right away. On Windows, you may need to install the adb (Android Debug Bridge) driver. If Windows cannot find the adb driver, then download one from your device’s manufacturer’s website. You can confirm that your device is recognized by opening the Devices view. The quickest way to the Devices view is to open the DDMS perspective by clicking the DDMS button at the top right corner of the Eclipse workbench. The Devices view should appear on the lefthand side of the workbench. You should see your AVD and your hardware device listed in this view. 44 Configuring your device for development To get back to the editor and other views, click the Java button at the top right corner of the workbench. If you are having trouble getting your device recognized, the first thing to try is resetting adb. In the Devices view, click on the downward pointing arrow at the top-right of the view. This will reveal a menu. The bottom choice is Reset�adb. Select this item, and, after a moment, your device may appear in the list. If resetting adb does not work, you can find more help on the Android developers site. Start at http://developer.android.com/tools/device.html. Or visit this book’s forum at forums.bignerdranch.com for more troubleshooting help. Configuring your device for development To test apps on your device, you need to set it up to accept applications that are not from Google Play. • On devices running Android 4.1 or earlier, open the device’s Settings and go to Applications. Make sure that Unknown�sources is checked. • On devices running Android 4.2, go to Settings → Security to find the Unknown�sources option. You also need to enable USB debugging on the device. • On devices running versions of Android earlier than 4.0, go to Settings → Applications → Development and find the option to enable USB�debugging. • On devices running Android 4.0 or 4.1, go to Settings → Developer�options instead. • On Android 4.2, Developer�options is not visible by default. To enable it, go to Settings → About Tablet/Phone and press Build�Number 7 times. Then you can return to Settings, see Developer options, and enable USB�debugging. As you can see, the options vary considerably across devices. If you are having problems enabling your device, visit http://developer.android.com/tools/device.html for more help. Run GeoQuiz as before. Eclipse will offer a choice between running on the virtual device or the hardware device plugged into your system. Select the hardware device and continue. GeoQuiz will launch on your device. (If you are not offered a choice and GeoQuiz starts in the emulator, recheck the steps above and make sure your device is plugged in.) Adding an Icon GeoQuiz is now up and running, but the user interface would be spiffier if the Next button also displayed a right-pointing arrow icon. You can find such an arrow in the solutions file for this book (http://www.bignerdranch.com/ solutions/AndroidProgramming.zip). The solutions file is a collection of Eclipse projects – one for each chapter. Download this file and open the 02_MVC/GeoQuiz/res directory. Within this directory, locate the drawable-hdpi, res/drawable-mdpi, and drawable-xhdpi directories. 45 Chapter 2 Android and Model-View-Controller The suffixes on these directory names refer to the screen pixel density of a device: mdpi medium-density screens (~160dpi) hdpi high-density screens (~240dpi) xhdpi extra-high-density screens (~320dpi) (There is also a low-density – ldpi – category, but most of the devices with low-density screens are now retired.) Within each directory, you will find two image files – arrow_right.png and arrow_left.png. These files have been customized for the screen pixel density specified in the directory’s name. In production apps, it is important to provide images for the different screen pixel densities in your projects. Providing different images will reduce artifacts from images being scaled up and down. All of the images in your project will be installed with your app, and the OS will choose the best one for that specific device. Adding resources to a project The next step is to add the image files to GeoQuiz’s resources. In the package explorer, reveal the contents of your res directory and find the three subdirectories whose names match those in the solutions file. Copy the files from each directory in the solutions to the corresponding directory in the package explorer. Figure 2.7 Arrow icons in GeoQuiz drawable directories 46 Referencing resources in XML Note that if you drag and drop a file, you will get a prompt like Figure 2.8. Choose to copy the file rather than link to it. Figure 2.8 Copy. Don’t link. Including images in your app is as simple as that. Any .png, .jpg, or .gif file you add to a res/ drawable folder will be automatically assigned a resource ID. (Note that filenames must be lower-case and not have any spaces.) After you have copied the files, open gen/R.java and look for your new resource IDs in the R.drawable inner class. There are only two new resource IDs: R.drawable.arrow_left and R.drawable.arrow_right. These resource IDs are not qualified by screen density. So you do not need to determine the device’s screen density at runtime. All you have to do is use this resource ID in your code. When the app is run, the OS will determine the appropriate image to display on that particular device. You will learn more about how Android resource system works starting in Chapter 3. For now, let’s put that right arrow to work. Referencing resources in XML You use resource IDs to reference resources in code. But you want to configure the Next button to display the arrow in the layout definition. How do you reference a resource from XML? With a slightly different syntax. Open activity_quiz.xml and add two attributes to the Button widget definition. 47 Chapter 2 Android and Model-View-Controller Listing 2.12 Adding an icon to the Next button (activity_quiz.xml)... In an XML resource, you refer to another resource by its resource type and name. A reference to a string resource begins with @string/. A reference to a drawable resource begins with @drawable/. You will learn more about naming resources and working in the res directory structure starting in Chapter 3. Run GeoQuiz. Admire your button’s new appearance. Then test it to make sure it still works as before. GeoQuiz does, however, have a bug. While GeoQuiz is running, press the Next button to show another question. Then rotate the device. (If you are running on the emulator, press Control+F12/Ctrl+F12 to rotate.) After you rotate, you will be looking at the first question again. How did this happen and how can you fix it? The answers to those questions have to do with the activity lifecycle, which is the topic of Chapter 3. Challenges Challenges are exercises at the end of the chapter for you to do on your own. Some are easy and provide practice doing the same thing you have done in the chapter. Other challenges are harder and require more problem-solving. We cannot encourage you enough to take on these challenges. Tackling them cements what you have learned, builds confidence in your skills, and bridges the gap between us teaching you Android programming and you being able to do Android programming on your own. If you get stuck while working on a challenge, take a break and come back and try again fresh. If that does not help, check out the forum for this book at forums.bignerdranch.com. In the forum, you can review questions and solutions that other readers have posted as well as ask questions and post solutions of your own. 48 Challenge: Add a Listener to the TextView To protect the integrity of your current project, we recommend you make a copy in Eclipse and work on challenges in the new copy. Right-click the project in the package explorer, select Copy, and then right-click again to paste the copied project. You will be prompted to name the new project, and then it will appear in the package explorer ready for work. Challenge: Add a Listener to the TextView Your Next button is nice, but you could also make it so that a user could press the TextView itself to see the next question. Hint: You can use the View.OnClickListener listener for the TextView that you have used with the Buttons because TextView also inherits from View. Challenge: Add a Previous Button Add a button that the user can press to go back one question. The UI should look something like Figure 2.9. Figure 2.9 Now with a previous button! This is a great challenge to do. It requires you to retrace on your own many of the steps in these two chapters. Challenge: From Button to ImageButton Perhaps the user interface would look even better if the next and previous buttons showed only icons. 49 Chapter 2 Android and Model-View-Controller Figure 2.10 Icon-only buttons To accomplish this challenge, these two widgets must become ImageButtons instead of regular Buttons. ImageButton TextView. is a widget that inherits from ImageView. Button, on the other hand, inherits from Figure 2.11 Inheritance diagram for ImageButton and Button 50 Challenge: From Button to ImageButton You can replace the text and drawable attributes on the Next button with a single ImageView attribute: Of course, you will need to modify QuizActivity to work with ImageButton. After you have changed these buttons to ImageButtons, Eclipse will warn you about a missing android:contentDescription attribute. This attribute supports accessibility for low-vision readers. You set the value to a string, and then that string is read aloud when users have the appropriate settings applied. Finally, add an android:contentDescription attribute to each ImageButton. 51 This page intentionally left blank 3 The Activity Lifecycle Every instance of Activity has a lifecycle. During this lifecycle, an activity transitions between three possible states: running, paused, and stopped. For each transition, there is an Activity method that notifies the activity of the change in its state. Figure 3.1 shows the activity lifecycle, states, and methods. Figure 3.1 Activity state diagram Subclasses of Activity can take advantage of the methods named in Figure 3.1 to get work done at critical transitions in the activity’s lifecycle. 53 Chapter 3 The Activity Lifecycle You are already acquainted with one of these methods – onCreate(Bundle). The OS calls this method after the activity instance is created but before it is put on screen. Typically, an activity overrides onCreate(…) to prepare the specifics of its user interface: • inflating widgets and putting them on screen (in the call to (setContentView(int)) • getting references to inflated widgets • setting listeners on widgets to handle user interaction • connecting to external model data It is important to understand that you never call onCreate(…) or any of the other Activity lifecycle methods yourself. You override them in your activity subclasses, and Android calls them at the appropriate time. Logging the Activity Lifecycle In this section, you are going to override lifecycle methods to eavesdrop on QuizActivity’s lifecycle. Each implementation will simply log a message informing you that the method has been called. Making log messages In Android, the android.util.Log class sends log messages to a shared system-level log. Log has several methods for logging messages . Here is the one that you will use most often in this book: public static int d(String tag, String msg) The d stands for “debug” and refers to the level of the log message. (There is more about the Log levels in the final section of this chapter.) The first parameter identifies the source of the message, and the second is the contents of the message. The first string is typically a TAG constant with the class name as its value. This makes it easy to determine the source of a particular message. In QuizActivity.java, add a TAG constant to QuizActivity: Listing 3.1 Adding TAG constant (QuizActivity.java) public class QuizActivity extends Activity { private static final String TAG = "QuizActivity"; ... } Next, in onCreate(…), call Log.d(…) to log a message. 54 Making log messages Listing 3.2 Adding log statement to onCreate(…) (QuizActivity.java) public class QuizActivity extends Activity { ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate(Bundle) called"); setContentView(R.layout.activity_quiz); ... This code may cause an error regarding the Log class. If so, press Command+Shift+O (Ctrl+Shift +O) to organize your imports. Eclipse will then ask you to choose which class to import. Choose android.util.Log. Now override five more methods in QuizActivity: Listing 3.3 Overriding more lifecycle methods (QuizActivity.java) } // End of onCreate(Bundle) @Override public void onStart() { super.onStart(); Log.d(TAG, "onStart() called"); } @Override public void onPause() { super.onPause(); Log.d(TAG, "onPause() called"); } @Override public void onResume() { super.onResume(); Log.d(TAG, "onResume() called"); } @Override public void onStop() { super.onStop(); Log.d(TAG, "onStop() called"); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy() called"); } } Notice that you call the superclass implementations before you log your messages. These superclass calls are required. Calling the superclass implementation before you do anything else is critical in onCreate(…); the order is less important in the other methods. 55 Chapter 3 The Activity Lifecycle You may have been wondering about the @Override annotation. This asks the compiler to ensure that the class actually has the method that you are attempting to override. For example, the compiler would be able to alert you to the following misspelled method name: public class QuizActivity extends Activity { @Override public void onCreat(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_quiz); } ... The Activity class does not have an onCreat(Bundle) method, so the compiler will complain. Then you can fix the typo rather than accidentally implementing QuizActivity.onCreat(Bundle). Using LogCat To access to the log while the application is running, you can use LogCat, a log viewer included in the Android SDK tools. To get to LogCat, select Window → Show�View → Other... In the Android folder, find and select LogCat and click OK (Figure 3.2). Figure 3.2 Finding LogCat LogCat will open in the right half of your screen and annoyingly shrink your editor. It would be better if LogCat were at the bottom of the workbench window. To get it there, drag from the tab of the LogCat view to the toolbar at the bottom-right corner of the workbench. 56 Using LogCat Figure 3.3 Drag from LogCat tab to bottom-right toolbar The LogCat view will close, and its icon (a horizontal Andy with Nyan rainbow feet) will appear in the toolbar. Click the icon, and LogCat will reopen at the bottom of the window. Your Eclipse workbench should now look something like Figure 3.4. You can resize the panes in LogCat by dragging their boundaries. This is true for any of the panes in the Eclipse workbench. 57 Chapter 3 The Activity Lifecycle Figure 3.4 Eclipse workbench – now with LogCat Run GeoQuiz, and messages will start appearing fast and furiously in LogCat. Most of the messages will be system output. Scroll to the bottom of the log to find your messages. In LogCat’s Tag column, you will see the TAG constant you created for QuizActivity. (If you do not see any messages in LogCat, LogCat may be monitoring the wrong device. Select Window → Show�View → Other... and open the Devices view. Select the device you are currently running on and then return to LogCat.) To make your messages easier to find, you can filter the output using the TAG constant. In LogCat, click the green + button at the top of the lefthand pane to create a message filter. Name the filter QuizActivity and enter QuizActivity in the by�Log�Tag: field (Figure 3.5). 58 Using LogCat Figure 3.5 Creating a filter in LogCat Click OK, and a new tab will open showing only messages tagged QuizActivity (Figure 3.6). Three lifecycle methods were called after GeoQuiz was launched and the initial instance of QuizActivity was created. Figure 3.6 Launching GeoQuiz creates, starts, and resumes an activity (If you are not seeing the filtered list, select the QuizActivity filter from LogCat’s lefthand pane.) Now let’s have some fun. Press the Back button on the device and then check LogCat. Your activity received calls to onPause(), onStop(), and onDestroy(). Figure 3.7 Pressing the Back button destroys the activity When you pressed the Back button, you told Android, “I’m done with this activity, and I won’t need it anymore.” Android then destroyed your activity. This is Android’s way of being frugal with your device’s limited resources. 59 Chapter 3 The Activity Lifecycle Relaunch GeoQuiz. Press the Home button and then check LogCat. Your activity received calls to onPause() and onStop(), but not onDestroy(). Figure 3.8 Pressing the Home button stops the activity On the device, pull up the task manager. On newer devices, press the Recents button next to the Home button (Figure 3.9). On devices without a Recents button, long-press the Home button. Figure 3.9 Home, Back, and Recents buttons In the task manager, press GeoQuiz and then check LogCat. The activity was started and resumed, but it did not need to be created. Pressing the Home button tells Android, “I’m going to go look at something else, but I might come back.” Android pauses and stops your activity but tries not to destroy it in case you come back. However, a stopped activity’s survival is not guaranteed. When the system needs to reclaim memory, it will destroy stopped activities. 60 Rotation and the Activity Lifecycle Finally, imagine a small pop-up window that only partially covers the activity. When one of these appears, the activity behind it is paused and cannot be interacted with. The activity will be resumed when the pop-up window is dismissed. As you continue through the book, you will override the different activity lifecycle methods to do real things for your application. When you do, you will learn more about the uses of each method. Rotation and the Activity Lifecycle Let’s get back to the bug you found at the end of Chapter 2. Run GeoQuiz, press the Next button to reveal the second question, and then rotate the device. (For the emulator, press Control+F12/Ctrl+F12 to rotate.) After rotating, GeoQuiz will display the first question again. Check LogCat to see what has happened. Figure 3.10 QuizActivity is dead. Long live QuizActivity! When you rotated the device, the instance of QuizActivity that you were looking at was destroyed, and a new one was created. Rotate the device again to witness another round of destruction and rebirth. This is the source of your bug. Each time a new QuizActivity is created, mCurrentIndex is initialized to 0, and the user starts over at the first question. You will fix this bug in a moment. First, let’s take a closer look at why this happens. Device configurations and alternative resources Rotating the device changes the device configuration. The device configuration is a set of characteristics that describe the current state of an individual device. The characteristics that make up the configuration include screen orientation, screen density, screen size, keyboard type, dock mode, language, and more. Typically, applications provide alternative resources to match different device configurations. You saw an example of this when you added multiple arrow icons to your project for different screen densities. Screen density is a fixed component of the device configuration; it cannot change at runtime. On the other hand, some components, like screen orientation, can change at runtime. When a runtime configuration change occurs, there may be resources that are a better match for the new configuration. To see this in action, let’s create an alternative resource for Android to find and use when the device’s screen orientation changes to landscape. 61 Chapter 3 The Activity Lifecycle Creating a landscape layout First, minimize LogCat. (If you accidentally close LogCat instead, you can always re-open it from Window → Show�View...) Next, in the package explorer, right-click the res directory and create a new folder. Name this folder layout-land. Figure 3.11 Creating a new folder Copy the activity_quiz.xml file from res/layout/ to res/layout-land/. You now have a landscape layout and a default layout. Keep the filename the same. The two layout files must have the same filename so that they can be referenced with the same resource ID. The -land suffix is another example of a configuration qualifier. Configuration qualifiers on res subdirectories are how Android identifies which resources best match the current device configuration. You can find the list of configuration qualifiers that Android recognizes and the pieces of the device configuration that they refer to at http://developer.android.com/guide/topics/resources/ 62 Device configurations and alternative resources providing-resources.html. You will also get more practice working with configuration qualifiers in Chapter 15. When the device is in landscape orientation, Android will find and use resources in the res/ layout-land directory. Otherwise, it will stick with the default in res/layout/. Let’s make some changes to the landscape layout so that it is different from the default. Figure 3.12 shows the changes that you are going to make. Figure 3.12 An alternative landscape layout The FrameLayout will replace the LinearLayout. FrameLayout is the simplest ViewGroup and does not arrange its children in any particular manner. In this layout, child views will be arranged according to their android:layout_gravity attributes. The TextView, LinearLayout, and Button need android:layout_gravity attributes. The Button children of the LinearLayout will stay exactly the same. Open layout-land/activity_quiz.xml and make the necessary changes using Figure 3.12. You can use Listing 3.4 to check your work. 63 Chapter 3 The Activity Lifecycle Listing 3.4 Tweaking the landscape layout (layout-land/activity_quiz.xml)... Run GeoQuiz again. Rotate the device to landscape to see the new layout. Of course, this is not just a new layout; it is a new QuizActivity as well. 64 Saving Data Across Rotation Figure 3.13 QuizActivity in landscape orientation Rotate back to portrait to see the default layout and yet another new QuizActivity. Android does the work of determining the best resource for you, but it has to create a new activity from scratch to do it. For a QuizActivity to display a different layout, setContentView(R.layout.activity_quiz) must be called again. And this will not happen unless QuizActivity.onCreate(…) is called again. Thus, Android destroys the current QuizActivity on rotation and starts fresh to ensure that it has the resources that best match the new configuration. Note that Android destroys the current activity and creates a new one whenever any runtime configuration change occurs. A change in keyboard availability or language could also occur at runtime, but a change in screen orientation is the runtime change that occurs most frequently. Saving Data Across Rotation Android does a great job of providing alternative resources at the right time. However, destroying and recreating activities on rotation can cause headaches, too, like GeoQuiz’s bug of reverting back to the first question when the device is rotated. To fix this bug, the post-rotation QuizActivity needs to know the old value of mCurrentIndex. You need a way to save this data across a runtime configuration change, like rotation. One way to do this is to override the Activity method protected void onSaveInstanceState(Bundle outState) This method is normally called by the system before onPause(), onStop(), and onDestroy(). The default implementation of onSaveInstanceState(…) asks all of the activity’s views to save their state as data in the Bundle object. A Bundle is a structure that maps string keys to values of certain limited types. 65 Chapter 3 The Activity Lifecycle You have seen this Bundle before. It is passed into onCreate(Bundle) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... When you override onCreate(…), you call onCreate(…) on the activity’s superclass and pass in the bundle you just received. In the superclass implementation, the saved state of the views is retrieved and used to recreate the activity’s view hierarchy. Overriding onSaveInstanceState(Bundle) You can override onSaveInstanceState(…) to save additional data to the bundle and then read that data back in onCreate(…). This is how you are going to save the value of mCurrentIndex across rotation. First, in QuizActivity.java, add a constant that will be the key for the key-value pair that will be stored in the bundle. Listing 3.5 Adding a key for the value (QuizActivity.java) public class QuizActivity extends Activity { private static final String TAG = "QuizActivity"; private static final String KEY_INDEX = "index"; Button mTrueButton; ... Next, override onSaveInstanceState(…) to write the value of mCurrentIndex to the bundle with the constant as its key. Listing 3.6 Overriding onSaveInstanceState(…) (QuizActivity.java) mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; updateQuestion(); } }); } updateQuestion(); @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); Log.i(TAG, "onSaveInstanceState"); savedInstanceState.putInt(KEY_INDEX, mCurrentIndex); } 66 The Activity Lifecycle, Revisited Finally, in onCreate(…), check for this value. If it exists, assign it to mCurrentIndex. Listing 3.7 Checking bundle in onCreate(…) (QuizActivity.java) ... if (savedInstanceState != null) { mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0); } } updateQuestion(); Run GeoQuiz and press Next. No matter how many device or user rotations you perform, the newlyminted QuizActivity will “remember” what question you were on. Note that the types that you can save to and restore from a Bundle are primitive types and objects that implement the Serializable interface. When you are creating custom classes, be sure to implement Serializable if you want to save them in onSaveInstanceState(…). Testing the implementation of onSaveInstanceState(…) is a good idea – especially if you are saving and restoring objects. Rotation is easy to test; testing low-memory situations is harder. There is information at the end of this chapter about how to simulate your activity being destroyed by Android to reclaim memory. The Activity Lifecycle, Revisited Overriding onSaveInstanceState(Bundle) is not just for handling rotation. An activity can also be destroyed if the user navigates away for a while and Android needs to reclaim memory. Android will never destroy a running activity to reclaim memory – the activity must be in the paused or stopped state to be destroyed. If an activity is paused or stopped, then its onSaveInstanceState(…) method has been called. When onSaveInstanceState(…) is called, the data is saved to the Bundle object. That Bundle object is then stuffed into your activity’s activity record by the OS To understand the activity record, let’s add a stashed state to the activity lifecycle (Figure 3.14). 67 Chapter 3 The Activity Lifecycle Figure 3.14 The complete activity lifecycle When your activity is stashed, an Activity object does not exist, but the activity record object lives on in the OS. The OS can reanimate the activity using the activity record when it needs to. Note that your activity can pass into the stashed state without onDestroy() being called. However, you can always rely on onPause() and onSaveInstanceState(…) to be called. Typically, you override onSaveInstanceState(…) to stash data in your Bundle and onPause() for anything else that needs to be done. Under some situations, Android will not only kill your activity but also completely shut down your application’s process. This will only happen if the user is not currently looking at your application, but it can (and does) happen. Even in this case, the activity record will live on and enable a quick restart of your activity if the user returns. So when does the activity record get snuffed? When the user presses the Back button, your activity really gets destroyed, once and for all. At that point, your activity record is discarded. Activity records are also typically discarded on reboot and may also be discarded if they are not used for a long time. 68 For the More Curious: Testing onSaveInstanceState(Bundle) For the More Curious: Testing onSaveInstanceState(Bundle) If you are overriding onSaveInstanceState(Bundle), you should test that your state is being saved and restored as expected. This is easy to do on the emulator. Start up a virtual device. Within the list of applications on the device, find the Settings app. This app is included with most system images used on the emulator. Figure 3.15 Finding the Settings app Launch Settings and select Development�options. Here you will see many possible settings. Turn on the setting labeled Don’t�keep�activities. 69 Chapter 3 The Activity Lifecycle Figure 3.16 Don’t keep activities selected Now run your app and press the Home button. Pressing Home causes the activity to be paused and stopped. Then the stopped activity will be destroyed just as if the Android OS had reclaimed it for its memory. Then you can restore the app to see if your state was saved as you expected. Pressing the Back button instead of the Home button will always destroy the activity regardless of whether you have this development setting on. Pressing the Back button tells the OS that the user is done with the activity. To run the same test on a hardware device, you must install Dev Tools on the device. For more information, visit http://developer.android.com/tools/debugging/debugging-devtools.html. For the More Curious: Logging Levels and Methods When you use the android.util.Log class to send log messages, you control not only the content of a message, but also a level that specifies how important the message is. Android supports five log levels, shown in Figure 3.17. Each level has a corresponding method in the Log class. Sending output to the log is as simple as calling the corresponding Log method. 70 For the More Curious: Logging Levels and Methods Figure 3.17 Log levels and methods In addition, each of the logging methods has two signatures: one which takes a tag string and a message string and a second that takes those two arguments plus an instance of Throwable, which makes it easy to log information about a particular exception that your application might throw. Listing 3.8 shows some sample log method signatures. Use regular Java string concatenation to assemble your message string, or String.format if you have fancier needs. Listing 3.8 Different ways of logging in Android // Log a message at "debug" log level Log.d(TAG, "Current question index: " + mCurrentIndex); TrueFalse question; try { question = mQuestionBank[mCurrentIndex]; } catch (ArrayIndexOutOfBoundsException ex) { // Log a message at "error" log level, along with an exception stack trace Log.e(TAG, "Index was out of bounds", ex); } 71 This page intentionally left blank 4 Debugging Android Apps In this chapter, you will find out what to do when apps get buggy. You will learn how to use LogCat, Android Lint, and the debugger that comes with Eclipse. To practice debugging, the first step is to break something. In QuizActivity.java, comment out the code in onCreate(Bundle) where you pull out mQuestionTextView. Listing 4.1 Comment out a crucial line (QuizActivity.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); //mQuestionTextView = (TextView)findViewById(R.id.question_text_view); mTrueButton = (Button)findViewById(R.id.true_button); mTrueButton.setOnClickListener(new View.OnClickListener() { ... }); } ... Run GeoQuiz and see what happens. 73 Chapter 4 Debugging Android Apps Figure 4.1 GeoQuiz is about to E.X.P.L.O.D.E. Figure 4.1 shows the message that appears when your app crashes and burns. Different versions of Android will have slightly different messages, but they all mean the same thing. Of course, you know what is wrong with your app, but if you did not, it might help to look at your app from a new perspective. The DDMS Perspective From the Eclipse menu, select Window → Open�Perspective → DDMS. 74 The DDMS Perspective Figure 4.2 DDMS Perspective A perspective is a pre-defined set of views in Eclipse. You usually want to see different views when you are debugging than when you are editing, so Eclipse puts each set of views together into a perspective. Perspectives are pre-defined, but they are not set in stone. You can add and remove views from any of them, and Eclipse will remember your choices. If you ever need to start fresh, you can return a perspective to its default state by choosing Window → Reset�Perspective... The default perspective where you have been editing code is called the Java perspective. The perspectives that you currently have open are listed near the top-right of the Eclipse workbench, and you can click on those buttons to switch between them. Figure 4.2 shows the DDMS perspective. DDMS stands for Dalvik Debug Monitor Service. Behind the scenes, DDMS does the footwork for all Android debugging. The DDMS perspective includes LogCat and the Devices view. The Devices view shows you which devices and virtual devices are connected to your computer. Problems you are having with a specific device can usually be solved in this view. For example, is your device not showing up as an option when you run an application? Click the downward-pointing triangle at the top-right corner and select Reset�adb. Often, rebooting adb will find your device. Or is LogCat showing output for the wrong device? No problem – click to select the device you want to see output from, and LogCat will switch to showing output from that device. 75 Chapter 4 Debugging Android Apps Exceptions and Stack Traces Now back to your app’s problem. Expand the LogCat pane so that you can see what has happened. If you scroll up and down in LogCat, you should eventually find an expanse of red. This is a standard AndroidRuntime exception report. Figure 4.3 Exception and stack trace in LogCat The report tells you the top-level exception and its stack trace, then the exception that caused that exception and its stack trace, and so on and so forth until it finds an exception with no cause. In most of the code you will write, that last exception with no cause is the interesting one. Here the exception without a cause is a java.lang.NullPointerException. The line just below this exception is the first line in its stack trace. This line tells you the class and method where the exception occurred as well as what file and line number the exception occurred on. Double-click this line, and Eclipse will take you to that line in your source code. The line to which you are taken is the first use of the mQuestionTextView variable, inside updateQuestion(). The name NullPointerException gives you a hint to the problem: this variable was not initialized. Uncomment the line initializing mQuestionTextView to fix the bug. When you run into runtime exceptions, remember to look for the last exception in LogCat and the first line in its stack trace that refers to code that you have written. That is where the problem occurs, and it is the best place to start looking for answers. 76 Diagnosing misbehaviors If a crash occurs on a device while it is not plugged in, all is not lost. The device will store the latest lines written to the log. The length and expiration of the stored log depends on the device, but you can usually count on retrieving log results within ten minutes. Just plug in the device, pull up DDMS in Eclipse, and select your device in the Devices view. LogCat will fill itself with the stored log. Diagnosing misbehaviors Problems with your apps will not always be crashes. In some cases, they will be misbehaviors. For example, suppose that every time you pressed the Next button, nothing happened. That would be a noncrashing, misbehaving bug. In QuizActivity.java, make a change to the mNextButton listener to comment out the code that increments mCurrentIndex. Listing 4.2 Forget a critical line of code (QuizActivity.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... mNextButton = (Button)findViewById(R.id.next_button); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; //mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; updateQuestion(); } }); } ... Run GeoQuiz and press the Next button. You should see no effect. 77 Chapter 4 Debugging Android Apps Figure 4.4 Pressing Next does nothing This bug is trickier than the last bug. It is not throwing an exception, so fixing the bug is not a simple matter of making the exception go away. On top of that, this misbehavior could be caused in two different ways: the index might not be changing, or updateQuestion() might not be called. If you had no idea what was causing the problem, you would need to track down the culprit. In the next few sections, you will see two ways to do this: diagnostic logging of a stack trace and using the debugger to set a breakpoint. Logging stack traces In QuizActivity, add a log statement to updateQuestion(). Listing 4.3 Exception for fun and profit (QuizActivity.java) public class QuizActivity extends Activity { ... public void updateQuestion() { Log.d(TAG, "Updating question text for question #" + mCurrentIndex, new Exception()); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } The Log.d(String, String, Throwable) version of Log.d logs the entire stack trace just like with the AndroidRuntime exception you saw earlier. The stack trace will tell you where the call to updateQuestion() was made. 78 Logging stack traces The exception that you pass to Log.d(…) does not have to be a thrown exception that you caught. You can create a brand new Exception() and pass it to the method without ever throwing it, and you will get a report of where the exception was created. Run GeoQuiz, press the Next button, and then check the output in LogCat. Figure 4.5 The results The top line in the stack trace is the line where you logged out the Exception. Two lines after that you can see where updateQuestion() was called from within your onClick(…) implementation. Doubleclick this line, and you will be taken to where you commented out the line to increment your question index. But do not get rid of the bug; you are going to use the debugger to find it again in a moment. Logging out stack traces is a powerful tool, but it is also a verbose and revealing one. Leave a bunch of these hanging around, and soon LogCat will be an unmanageable mess. Also, a competitor might steal your ideas by reading your stack traces to understand what your code is doing. On the other hand, sometimes a stack trace showing what your code does is exactly what you need. If you are seeking help with a problem at http://stackoverflow.com or forums.bignerdranch.com, it often helps to include a stack trace. You can copy and paste lines directly from LogCat or save selected output to a text file by clicking the button that looks like a floppy disk at the top-right of the LogCat view. Before continuing, comment out the TAG constant and delete the log statement in QuizActivity.java. Listing 4.4 Farewell, old friend (QuizActivity.java) public class QuizActivity extends Activity { ... public void updateQuestion() { Log.d(TAG, "Updating question text for question #" + mCurrentIndex, new Exception()); int question = mQuestionBank[mCurrentIndex].getQuestion(); mQuestionTextView.setText(question); } 79 Chapter 4 Debugging Android Apps Commenting out TAG will remove the unused variable warning. You could delete it, but you never know when you will need it again to log messages. Setting breakpoints Try using the debugger that comes with Eclipse to track down the same bug. You can set a breakpoint on updateQuestion() to see whether it was called. A breakpoint will pause execution before this line executes and allow you to examine line-by-line what happens next. In QuizActivity.java, return to the updateQuestion() method. In the first line of this method, double-click the gray bar in the lefthand margin. You should now see a blue circle in the gray bar. This is a breakpoint. Figure 4.6 A breakpoint To engage the debugger and trigger your breakpoint, you need to debug your app instead of running it. To debug your app, right-click on the GeoQuiz project and select Debug�As → Android�Application. Your device will report that it is waiting for the debugger to attach, and then it will proceed normally. Once your app is up and running with the debugger attached, your app will pause. Firing up GeoQuiz called QuizActivity.onCreate(Bundle), which called updateQuestion(), which hit your breakpoint. If this is the first time you have used the debugger, you will now see a large rectangle of words about opening the Debug perspective. Open the Debug perspective by clicking Yes. Figure 4.7 Switch to the Debug perspective Eclipse will then open the Debug perspective. In the middle of the Debug perspective is an editor view. You can see that this editor has opened QuizActivity.java and highlighted the line with the breakpoint where execution has paused. Above the editor is the Debug view. This view shows the current stack trace. 80 Setting breakpoints Figure 4.8 The Debug view You can use the buttons with the yellow arrows at the top of the view to step through your program. You can see from this stack trace that updateQuestion() has been called from inside onCreate(Bundle). You are interested in investigating the Next button behavior, though, so click the Resume button to continue execution. Then press the Next button again to see if your breakpoint is activated (it should be). Now that you are at an interesting point of execution, you can take a look around. At the top right, find the Variables view. Here you can examine the values of the objects in your program. When you first see this view, you will only see one value: this (the QuizActivity itself). Click the disclosure triangle next to this or press the right arrow key to expand it. It should look something like Figure 4.9: 81 Chapter 4 Debugging Android Apps Figure 4.9 A view of your variables The colorful shapes next to the variable names tell you the variable’s visibility: green circle blue triangle yellow diamond red square public variable default variable (visible within the same package) protected variable private variable Your expanded this is a little overwhelming. Your this view includes more than just the instance variables declared in QuizActivity. It also includes all the variables declared in its superclass, Activity, in Activity’s superclass, in its super-superclass, and so on. You are only interested in one value: mCurrentIndex. Scroll down in the variables view until you see mCurrentIndex. Sure enough, it still has a value of 0. This code looks perfectly fine. To continue your investigation, you need to step out of this method. Click the button to the right of the Step�Over button, whose tooltip reads Step�Return. (There is a helper method called access$1(QuizActivity) that also gets called, so you will need to click Step Return twice.) Check the editor view. It has now jumped you over to your mNextButton’s OnClickListener, right after updateQuestion() was called. Pretty nifty. You will want to fix this implementation, but before you make any changes to code, you need to stop debugging your app. You can do this in two ways: you can either stop the program, or you can simply disconnect the debugger. To stop the program, you choose your program’s running process inside the DDMS Devices view and click the red stop sign button to kill the process. Usually it is easier to simply disconnect. To do that, click the Disconnect button labeled above in Figure 4.8. Then switch back to the Java perspective and return your OnClickListener to its former glory. 82 Using exception breakpoints Listing 4.5 Returning to normalcy (QuizActivity.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... mNextButton = (Button)findViewById(R.id.next_button); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; updateQuestion(); } }); } ... You also need to remove your breakpoint. Select the Breakpoints view next to the Variables view. (If you do not see this view, you can open it by selecting Window → Show�View → Breakpoints.) From there, you can select the breakpoint and click the dark grey x button at the top of the view. You have tried out two ways of tracking down a misbehaving line of code: stack trace logging and setting a breakpoint in the debugger. Which is better? Each has its uses, and one or the other will probably end up being your favorite. Logging out stack traces has the advantage that you can see stack traces from multiple places in one log. The downside is that to learn something new you have to add new log statements, rebuild, deploy, and navigate through your app to see what happened. The debugger is more convenient. If you run your app with the debugger attached (by selecting Debug�As → Android�Application), then you can set a breakpoint while the application is still running and poke around to find out information about multiple issues. Using exception breakpoints If that were not enough decisions to make, you can also use the debugger to catch exceptions. Go back and comment out the line where you assign mQuestionTextView one more time. Then select Run → Add�Java�Exception�Breakpoint... to pull up the exception breakpoint dialog. 83 Chapter 4 Debugging Android Apps Figure 4.10 Setting an exception breakpoint This dialog allows you to set a breakpoint that is triggered when an exception is thrown, wherever it might happen. You can limit it to only uncaught exceptions, or apply it to both caught and uncaught exceptions. In Android, most exceptions will be caught by the framework, which will then show you the dialog you saw earlier and shut down your process. That means you will usually want to select the Suspend�on caught�exceptions option for your exception breakpoints. Next, choose what kind of exception to catch. Type in RuntimeException and choose any of the selections that come up. RuntimeException is the superclass of NullPointerException, ClassCastException, and other runtime problems you can run into, so it makes for a nice catch-all. It will not catch those subclass exception types until you do one more thing, though. Switch to the Debug perspective and look at your Breakpoints view. You should see your shiny new RuntimeException breakpoint there. Click on it and check Subclasses�of�this�exception so that your debugger will break on NullPointerException, too. Now debug GeoQuiz. This time, your debugger will jump right to the line where the exception was thrown as soon as it happens. Exquisite. Now, this is a fairly big hammer. If you leave this breakpoint on while debugging, you can expect it to stop on some framework code, or in other places you do not expect. So you may want to turn it off when you are not using it. File Explorer The DDMS perspective offers other power tools for checking out what your application is doing. One particularly handy one is the file explorer. Click the File�Explorer view from the tab group on the right. 84 Android-Specific Debugging Figure 4.11 The file explorer You can use this tool to browse around a device’s file system and download and upload files. On a physical device, you will not be able to see inside /data, but on an emulator you can – which means that you can peek into your app’s personal storage area. In the latest version of Android, that storage area is in /data/data/[your package name]. Figure 4.12 GeoQuiz’s data directory (in an emulator) There is nothing to see right now, but in Chapter 17 you will be able to see the files you write to private storage here. Android-Specific Debugging Most Android debugging is just like Java debugging. However, you will run into issues with Androidspecific parts, such as resources, that the Java compiler knows nothing about. Using Android Lint This is where Android Lint comes in. Android Lint is a static analyzer for Android code. A static analyzer is a program that examines your code to find defects without running it. Android Lint uses 85 Chapter 4 Debugging Android Apps its knowledge of the Android frameworks to look deeper into your code and find problems that the compiler cannot. In most cases, Android Lint’s advice is worth taking. In Chapter 6, you will see Android Lint warn you about compatibility problems. Android Lint can also perform type-checking for objects that are defined in XML. Make the following casting mistake in QuizActivity: Listing 4.6 A simple mix-up (QuizActivity.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); mTrueButton = (Button)findViewById(R.id.true_button); mTrueButton = (Button)findViewById(R.id.question_text_view); } ... Because you used the wrong resource ID, this code will attempt to cast a TextView as a Button at runtime. This will cause an improper cast exception. The Java compiler sees no problem with this code, but Android Lint will catch this error before you run your app. In the package explorer, right-click the GeoQuiz project and select Android�Tools → Run�Lint:�Check for�Common�Errors to see the Lint�Warnings view. Figure 4.13 Lint warnings Here you can see Android Lint reporting one error and one warning. You already know about the error: with the wrong resource ID, the cast to Button is guaranteed to fail. Go ahead and correct the cast in onCreate(Bundle). 86 Issues with the R class Listing 4.7 Fixing that simple mix-up (QuizActivity.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); mQuestionTextView = (TextView)findViewById(R.id.question_text_view); mTrueButton = (Button)findViewById(R.id.question_text_view); mTrueButton = (Button)findViewById(R.id.true_button); } ... The warning from Android Lint is less consequential. This warning suggests using a merge tag for layout-land/activity_quiz.xml. Unfortunately, Android Lint is wrong here. You should not replace FrameLayout with a merge tag here because you are relying on FrameLayout to position your widgets in a specific way. Issues with the R class You are familiar with build errors that occur when you reference resources before adding them or delete resources that other files refer to. Usually, resaving the files once the resource is added or the references are removed will cause Eclipse to rebuild without any fuss. Sometimes, however, these build errors will persist or appear seemingly out of nowhere. If this happens to you, here are some things you can try: Run Android Lint Select Window → Run�Android�Lint. This can often pull the pieces of your project together. Clean your project Select Project → Clean. Eclipse will rebuild the project from scratch, which often results in an error-free build. Recheck the validity of the XML in your resource files If your R.java file was not generated for the last build, you will see errors in your project wherever you reference a resource. Often, this is caused by an XML typo in one of your XML files. Layout XML is not validated, so typos in these files will not be pointedly brought to your attention. Finding the typo and resaving the file should cause R.java to regenerate. Delete your gen directory If you cannot get Eclipse to generate a new R.java file, you can delete the entire gen directory. When Eclipse rebuilds, it will create a new gen directory with a fully-functional R class. 87 Chapter 4 Debugging Android Apps If you are still having problems with resources or having different problems, give the error messages and your layout files a fresh look. It is easy to miss mistakes in the heat of the moment. Check out any Android Lint errors and warnings as well. A cool-headed reconsideration of the error messages may turn up a bug or typo. Finally, if you are stuck or having other issues with Eclipse, check the archives at http:// stackoverflow.com or visit the forum for this book at http://forums.bignerdranch.com. 88 5 Your Second Activity In this chapter, you will add a second activity to the GeoQuiz app. An activity controls a screen of information, and this activity will add a second screen that offers users a chance to see the answer to the current question. Figure 5.1 CheatActivity offers the chance to peek at the answer If the user chooses to view the answer and then returns to the QuizActivity and answers the question, he or she will get a new message. 89 Chapter 5 Your Second Activity Figure 5.2 QuizActivity knows if you’ve been cheating Why is this a good Android programming exercise? You will learn how to • create a new activity and a new layout without the help of the new application wizard • start an activity from another activity. Starting an activity means asking the OS to create an activity instance and call its onCreate(Bundle) method. • pass data between the parent (starting) activity and the child (started) activity Setting Up a Second Activity There is a lot to do in this chapter. You are going to start by creating a new layout for CheatActivity and then create the CheatActivity class itself. But first, open strings.xml and add all the strings you will need for this chapter. 90 Creating a new layout Listing 5.1 Adding strings (strings.xml) ... ... Creating a new layout The screenshot at the beginning of the chapter shows you what CheatActivity’s view should look like. Figure 5.3 shows the widget definitions. Figure 5.3 Diagram of layout for CheatActivity To create the layout file, right-click on the res/layout directory in the package explorer and select New → Other... Within the Android folder, find and select Android�XML�Layout�File (Figure 5.4). Then click Next. 91 Chapter 5 Your Second Activity Figure 5.4 Creating a new layout file In the dialog that appears, name the layout file activity_cheat.xml and select LinearLayout as its root element. Then click Finish. 92 Creating a new layout Figure 5.5 Naming and configuring new layout file When the file opens, navigate to the XML. Notice that this wizard included the line at the top of the file. This line is not required for XML layout files, but it is still added to layout files created from some sources, like this layout wizard. (By the way, if you are allergic to GUIs, you do not have to use the layout wizard. You can just create a new file named activity_cheat.xml in the res/layout directory and then refresh the res/layout folder so that Eclipse finds it. This is true for most of the Eclipse wizards. Feel free to create XML files and Java class files however you normally would. The new Android application wizard is the only wizard you cannot avoid.) The layout wizard created the root LinearLayout for you. You just need to add an android:gravity attribute and the three children. Try creating the XML for activity_cheat.xml using Figure 5.3 as a guide. After Chapter 8, we will only show layout diagrams like Figure 5.3 instead of long passages of XML, so it is good to start using them now to create your layout XML. You can check your work against Listing 5.2. 93 Chapter 5 Your Second Activity Listing 5.2 Filling out second activity’s layout (activity_cheat.xml)Lake Baikal is the world\'s oldest and deepest freshwater lake. Cheat! Are you sure you want to do this? Show Answer Cheating is wrong. Save your file and switch to the graphical layout tool to preview the layout. You will not be creating a landscape alternative for activity_cheat.xml, but there is a way to preview how the default layout will appear in landscape. In the graphical layout tool, find the button in the toolbar above the preview pane that looks like a device with a green arrow. Click this button to change the orientation of the preview. 94 Creating a new activity subclass Figure 5.6 Previewing activity_cheat.xml in landscape The default layout works well enough in both orientations, so let’s move on to creating the activity subclass. Creating a new activity subclass In the package explorer, right-click on the com.bignerdranch.android.geoquiz package and select New → Class. In the dialog that appears, name this class CheatActivity. In the Superclass: field, type android.app.Activity (Figure 5.7). 95 Chapter 5 Your Second Activity Figure 5.7 Create CheatActivity class Click Finish, and Eclipse will open CheatActivity.java in the editor. Add an implementation of onCreate(…) that passes the resource ID of the layout defined in activity_cheat.xml to setContentView(…). Listing 5.3 Overriding onCreate(…) (CheatActivity.java) public class CheatActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cheat); } } 96 Declaring activities in the manifest will eventually do more in its onCreate(…) method. But, for now, let’s move on to the next step: declaring CheatActivity in the application’s manifest. CheatActivity Declaring activities in the manifest The manifest is an XML file containing metadata that describes your application to the Android OS. The file is always named AndroidManifest.xml, and it lives in the root directory of your project. In the package explorer, find and open AndroidManifest.xml in the root directory of the project. Ignore the GUI editor and select the AndroidManifest.xml tab at the bottom of the editor. Every activity in an application must be declared in the manifest so that the OS can access it. When you used the new application wizard to create QuizActivity, the wizard declared the activity for you. For CheatActivity, you have to do it yourself. In AndroidManifest.xml, add a declaration for CheatActivity, as shown in Listing 5.4. Listing 5.4 Declaring CheatActivity in the manifest (AndroidManifest.xml) The android:name attribute is required, and the dot at the start of this attribute’s value tells the OS that this activity’s class is in the package specified in the package attribute in the manifest element at the top of the file. There are many interesting things in the manifest, but for now, let’s stay focused on getting CheatActivity up and running. You will learn about the different parts of the manifest in later chapters. 97 Chapter 5 Your Second Activity Adding a Cheat button to QuizActivity The plan is for the user to press a button in QuizActivity to get an instance of CheatActivity on screen. So you need new buttons in layout/activity_quiz.xml and layout-land/ activity_quiz.xml. In the default layout, add the new button as a direct child of the root LinearLayout. Its definition should come right before the Next button. Listing 5.5 Adding a Cheat! button to the default layout (layout/ activity_quiz.xml) ... In the landscape layout, have the new button appear at the bottom and center of the root FrameLayout. Listing 5.6 Adding a Cheat! button to the landscape layout (layout-land/ activity_quiz.xml) ... Save your layout files and reopen QuizActivity.java. Add a variable, get a reference, and set a View.onClickListener stub for the Cheat! button. 98 Starting an Activity Listing 5.7 Wiring up the Cheat! button (QuizActivity.java) public class QuizActivity extends Activity { ... private Button mNextButton; private Button mCheatButton; @Override protected void onCreate(Bundle savedInstanceState) { ... mCheatButton = (Button)findViewById(R.id.cheat_button); mCheatButton.setOnClickListener(new View.OnClickListener() { }); } @Override public void onClick(View v) { // Start CheatActivity } updateQuestion(); ... } Now you can get to the business of starting CheatActivity. Starting an Activity The simplest way one activity can start another is with the Activity method public void startActivity(Intent intent) You might guess that startActivity(…) is a class method that you call on the Activity subclass that you want to start. But it is not. When an activity calls startActivity(…), this call is sent to the OS. In particular, it is sent to a part of the OS called the ActivityManager. The ActivityManager then creates the Activity instance and calls its onCreate(…) method. Figure 5.8 Starting an activity 99 Chapter 5 Your Second Activity How does the ActivityManager know which Activity to start? That information is in the Intent parameter. Communicating with intents An intent is an object that a component can use to communicate with the OS. The only components you have seen so far are activities, but there are also services, broadcast receivers, and content providers. Intents are multi-purpose communication tools, and the Intent class provides different constructors depending on what you are using the intent to do. In this case, you are using an intent to tell the ActivityManager which activity to start, so you will use this constructor: public Intent(Context packageContext, Class> cls) The Class object specifies the activity that the ActivityManager should start. The Context object tells the ActivityManager which package the Class object can be found in. Figure 5.9 The intent: telling ActivityManager what to do Within mCheatButton’s listener, create an Intent that includes the CheatActivity class. Then pass the intent into startActivity(Intent) (Listing 5.8). Listing 5.8 Starting CheatActivity (QuizActivity.java) ... mCheatButton = (Button)findViewById(R.id.cheat_button); mCheatButton.setOnClickListener(new View.OnClickListener() { }); } 100 @Override public void onClick(View v) { Intent i = new Intent(QuizActivity.this, CheatActivity.class); startActivity(i); } updateQuestion(); Passing Data Between Activities Before starting the activity, the ActivityManager checks the package’s manifest for a declaration with the same name as the specified Class. If it finds a declaration, it starts the activity, and all is well. If it does not, you get a nasty ActivityNotFoundException. This is why all of your activities must be declared in the manifest. Explicit and implicit intents When you create an Intent with a Context and a Class object, you are creating an explicit intent. You use explicit intents to start activities within your application. It may seem strange that two activities within your application must communicate via the ActivityManager, which is outside of your application. However, this pattern makes it easy for an activity in one application to work with an activity in another application. When an activity in your application wants to start an activity in another application, you create an implicit intent. You will use implicit intents in Chapter 21. Run GeoQuiz. Press the Cheat! button, and an instance of your new activity will appear on screen. Now press the Back button. This will destroy the CheatActivity and return you to the QuizActivity. Passing Data Between Activities Now that you have a QuizActivity and a CheatActivity, you can think about passing data between them. The following diagram shows what data you will pass between the two activities. Figure 5.10 The conversation between QuizActivity and CheatActivity The QuizActivity will inform the CheatActivity of the answer to the current question when the CheatActivity is started. When the user presses the Back button to return to the QuizActivity, the CheatActivity will be destroyed. In its last gasp, it will send data to the QuizActivity about whether the user cheated. You will start with passing data from QuizActivity to CheatActivity. 101 Chapter 5 Your Second Activity Using intent extras To inform the CheatActivity of the answer to the current question, you will pass it the value of mQuestionBank[mCurrentIndex].isTrueQuestion(); You will send this value as an extra on the Intent that is passed into startActivity(Intent). Extras are arbitrary data that the calling activity can include with an intent. The OS forwards the intent to the recipient activity, which can then access the extra and retrieve the data. Figure 5.11 Intent extras: communicating with other activities An extra is structured as a key-value pair, like the one you used to save out the value of mCurrentIndex in QuizActivity.onSaveInstanceState(Bundle). To add an extra to an intent, you use Intent.putExtra(…). In particular, you will be calling public Intent putExtra(String name, boolean value) Intent.putExtra(…) comes in many flavors, but it always has two arguments. The always a String key, and the second argument is the value, whose type will vary. first argument is In CheatActivity.java, add a key for the extra. Listing 5.9 Add extra constant (CheatActivity.java) public class CheatActivity extends Activity { public static final String EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true"; ... An activity may be started from several different places, so you should define keys for extras on the activities that retrieve and use them. Using your package name as a qualifier for your extra, as seen in Listing 5.9, prevents name collisions with extras from other apps. 102 Using intent extras Next, return to QuizActivity and put the extra on the intent. Listing 5.10 Putting an extra on the intent (QuizActivity.java) ... mCheatButton.setOnClickListener(new View.OnClickListener() { }); } @Override public void onClick(View v) { Intent i = new Intent(QuizActivity.this, CheatActivity.class); boolean answerIsTrue = mQuestionBank[mCurrentIndex].isTrueQuestion(); i.putExtra(CheatActivity.EXTRA_ANSWER_IS_TRUE, answerIsTrue); startActivity(i); } updateQuestion(); You only need one extra, but you can put multiple extras on an Intent if you need to. To retrieve the value from the extra, you will use public boolean getBooleanExtra(String name, boolean defaultValue) The first argument is the name of the extra. The second argument of getBooleanExtra(…) is a default answer if the key is not found. In CheatActivity, retrieve the value from the extra and store it in a member variable. Listing 5.11 Using an extra (CheatActivity.java) public class CheatActivity extends Activity { ... private boolean mAnswerIsTrue; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cheat); } mAnswerIsTrue = getIntent().getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false); } Note that Activity.getIntent() returns the Intent object that was forwarded in startActivity(Intent). Finally, in CheatActivity, wire up the answer TextView and the Show�Answer button to use the retrieved value. 103 Chapter 5 Your Second Activity Listing 5.12 Enabling cheating (CheatActivity.java) public class CheatActivity extends Activity { ... private boolean mAnswerIsTrue; private TextView mAnswerTextView; private Button mShowAnswer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cheat); mAnswerIsTrue = getIntent().getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false); mAnswerTextView = (TextView)findViewById(R.id.answerTextView); } mShowAnswer = (Button)findViewById(R.id.showAnswerButton); mShowAnswer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mAnswerIsTrue) { mAnswerTextView.setText(R.string.true_button); } else { mAnswerTextView.setText(R.string.false_button); } } }); } This code is pretty straightforward. You set the TextView’s text using TextView.setText(int). TextView.setText(…) has many variations, and here you use the one that accepts the resource ID of a string resource. Run GeoQuiz. Press Cheat! to get to CheatActivity. Then press Show�Answer to reveal the answer to the current question. Getting a result back from a child activity At this point, the user can cheat with impunity. Let’s fix that by having the CheatActivity tell the QuizActivity whether the user chose to view the answer. When you want to hear back from the child activity, you call the following Activity method: public void startActivityForResult(Intent intent, int requestCode) The first parameter is the same intent as before. The second parameter is the request code. The request code is a user-defined integer that is sent to the child activity and then received back by the parent. It is used when an activity starts more than one type of child activity and needs to tell who is reporting back. 104 Getting a result back from a child activity In QuizActivity, modify mCheatButton’s listener to call startActivityForResult(Intent, int). Listing 5.13 Calling startActivityForResult(…) (QuizActivity.java) ... mCheatButton.setOnClickListener(new View.OnClickListener() { }); } @Override public void onClick(View v) { Intent i = new Intent(QuizActivity.this, CheatActivity.class); boolean answerIsTrue = mQuestionBank[mCurrentIndex].isTrueQuestion(); i.putExtra(CheatActivity.EXTRA_ANSWER_IS_TRUE, answerIsTrue); startActivity(i); startActivityForResult(i, 0); } updateQuestion(); QuizActivity will only ever start one you pass 0 for the required parameter. type of child activity. It does not matter what you send, so here Setting a result There are two methods you can call in the child activity to send data back to the parent: public final void setResult(int resultCode) public final void setResult(int resultCode, Intent data) Typically, the result code is one of two pre-defined constants: Activity.RESULT_OK or Activity.RESULT_CANCELED. (You can use another constant, RESULT_FIRST_USER, as an offset when defining your own result codes.) Setting result codes is useful when the parent needs to take different action depending on how the child activity finished. For example, if a child activity had an OK button and a Cancel button, the child activity would set a different result code depending on which button was pressed. Then the parent activity would take different action depending on the result code. Calling setResult(…) is not required of the child activity. If you do not need to distinguish between results or receive arbitrary data on an intent, then you can let the OS send a default result code. A result code is always returned to the parent if the child activity was started with startActivityForResult(…). If setResult(…) is not called, then, when the user presses the Back button, the parent will receive Activity.RESULT_CANCELED. Sending back an intent In this implementation, you are interested in passing some arbitrary data back to QuizActivity. So you are going to create an Intent, put an extra on it, and then call Activity.setResult(int, Intent) to get that data into QuizActivity’s hands. 105 Chapter 5 Your Second Activity Earlier, you defined the constant for the extra received by CheatActivity within CheatActivity. You will do the same for this new extra that CheatActivity will send to QuizActivity. This is because the incoming and outgoing extras both define the interface for CheatActivity. That way, if you want to use CheatActivity elsewhere in your app, you are referring to constants defined entirely within CheatActivity. In CheatActivity, add a constant for the extra’s key and a private method that creates an intent, puts an extra on it, and sets a result. Then call this method in the Show�Answer button’s listener. Listing 5.14 Setting a result (CheatActivity.java) public class CheatActivity extends Activity { public static final String EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true"; public static final String EXTRA_ANSWER_SHOWN = "com.bignerdranch.android.geoquiz.answer_shown"; ... private void setAnswerShownResult(boolean isAnswerShown) { Intent data = new Intent(); data.putExtra(EXTRA_ANSWER_SHOWN, isAnswerShown); setResult(RESULT_OK, data); } @Override protected void onCreate(Bundle savedInstanceState) { ... } // Answer will not be shown until the user // presses the button setAnswerShownResult(false); ... mShowAnswer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mAnswerIsTrue) { mAnswerTextView.setText(R.string.true_button); } else { mAnswerTextView.setText(R.string.false_button); } setAnswerShownResult(true); } }); } When the user presses the Show�Answer button, the CheatActivity packages up the result code and the intent in the call to setResult(int, Intent). Then, when the user presses the Back button to return to the QuizActivity, the ActivityManager calls the following method on the parent activity: protected void onActivityResult(int requestCode, int resultCode, Intent data) 106 Getting a result back from a child activity The parameters are the original request code from QuizActivity and the result code and intent passed into setResult(…). Figure 5.12 shows this sequence of interactions. Figure 5.12 Sequence diagram for GeoQuiz The final step is to override onActivityResult(int, result. int, Intent) in QuizActivity to handle the Handling a result In QuizActivity.java, add a new member variable to hold the value that CheatActivity is passing back. Then override onActivityResult(…) to retrieve it. 107 Chapter 5 Your Second Activity Listing 5.15 Implementing onActivityResult(…) (QuizActivity.java) public class QuizActivity extends Activity { ... private int mCurrentIndex = 0; private boolean mIsCheater; ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (data == null) { return; } mIsCheater = data.getBooleanExtra(CheatActivity.EXTRA_ANSWER_SHOWN, false); } } ... You can see in this implementation of onActivityResult(…) that QuizActivity has no interest in the request code or the result code. In other cases, you would write conditional code based on those values. Finally, modify the checkAnswer(boolean) method in QuizActivity to check whether the user cheated and respond appropriately. 108 How Android Sees Your Activities Listing 5.16 Changing toast message based on value of mIsCheater (QuizActivity.java) private void checkAnswer(boolean userPressedTrue) { boolean answerIsTrue = mQuestionBank[mCurrentIndex].isTrueQuestion(); int messageResId = 0; if (mIsCheater) { messageResId = R.string.judgment_toast; } else { if (userPressedTrue == answerIsTrue) { messageResId = R.string.correct_toast; } else { messageResId = R.string.incorrect_toast; } } } Toast.makeText(this, messageResId, Toast.LENGTH_SHORT) .show(); @Override protected void onCreate(Bundle savedInstanceState) { ... mNextButton = (Button)findViewById(R.id.next_button); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length; mIsCheater = false; updateQuestion(); } }); } ... Run GeoQuiz. Cheat and see what happens. How Android Sees Your Activities Let’s look at what is going on OS-wise as you move between activities. First, when you click on the GeoQuiz app in the launcher, the OS does not start the application; it starts an activity in the application. More specifically, it starts the application’s launcher activity. For GeoQuiz, QuizActivity is the launcher activity. When the template created the GeoQuiz application and QuizActivity, it made QuizActivity the launcher activity by default. Launcher activity status is specified in the manifest by the intent-filter element in QuizActivity’s declaration (Listing 5.17). 109 Chapter 5 Your Second Activity Listing 5.17 QuizActivity declared as launcher activity (AndroidManifest.xml) ... After the instance of QuizActivity is on screen, the user can press the Cheat! button. When this happens, an instance of CheatActivity is started – on top of the QuizActivity. These activities exist in a stack (Figure 5.13). Pressing the Back button in CheatActivity pops this instance off the stack, and the QuizActivity resumes its position at the top. Figure 5.13 GeoQuiz’s back stack 110 How Android Sees Your Activities A call to Activity.finish() in CheatActivity would also pop the CheatActivity off the stack. If you run GeoQuiz from Eclipse and press Back from the QuizActivity, the QuizActivity will be popped off the stack and you will return to the last screen you were viewing before running GeoQuiz. Figure 5.14 Looking at Home screen, running from Eclipse If you started GeoQuiz from the launcher application, pressing Back from QuizActivity will return you to the launcher. Figure 5.15 Running GeoQuiz from launcher 111 Chapter 5 Your Second Activity Pressing Back from the launcher will return you to the screen you were looking at before you opened the launcher. What you are seeing here is that the ActivityManager maintains a back stack and that this back stack is not just for your application’s activities. Activities for all applications share the back stack, which is one reason the ActivityManager is involved in starting your activities and lives with the OS and not your application. The stack represents the use of the OS and device as a whole rather than the use of a single application. (Wondering about the “up” button? We will discuss how to implement and configure this button in Chapter 16.) Challenge Cheaters never win. Unless, of course, they persistently circumvent your anti-cheating measures. Which they probably will. Because they are cheaters. GeoQuiz has a few major loopholes. For this challenge, you will busy yourself with closing them. Here the loopholes are in ascending order, from easiest to hardest to close: • Users can rotate CheatActivity after they cheat to clear out the cheating result. • Once they get back, users can rotate QuizActivity to clear out mIsCheater. • Users can press Next until the question they cheated on comes back around. Good luck! 112 6 Android SDK Versions and Compatibility Now that you have gotten your feet wet with the GeoQuiz app, let’s review some background material about the different versions of Android. The information in this chapter is important to have under your belt as you continue with the book and develop more complex and realistic apps. Android SDK Versions Table 6.1 shows the SDK versions, the associated versions of the Android firmware, and the percentages of devices currently running them as of March 2013. Table 6.1 Android API levels, firmware versions, and % of devices in use API level 17 16 Codename Jelly Bean Device firmware version % of devices in use 4.2 1.6 4.1 14.9 15 Ice Cream Sandwich (ICS) 4.0.3, 4.0.4 28.6 13 Honeycomb (released for tablets only) 3.2 0.9 3.1.x 0.3 2.3.3 - 2.3.7 43.9 2.3.2, 2.3.1, 2.3 0.2 12 10 9 Gingerbread 8 Froyo 2.2.x 7.5 7 Eclair 2.1.x 1.9 Each “codenamed” release is followed by incremental releases. For instance, Ice Cream Sandwich was initially released as Android 4.0 (API level 14). It was almost immediately replaced with incremental releases culminating in Android 4.0.3 and 4.0.4 (API level 15). The percentages shown in Table 6.1 will change, of course, but they do reveal an important trend: Android devices running older versions are not immediately upgraded or replaced when a newer version is available. As of March 2013, half of devices are still running Froyo or Gingerbread SDK versions. Android 2.3.7 (the last Gingerbread update) was released in September 2011. In contrast, only 1.6% of devices are running Android 4.2, which was released in November 2012. 113 Chapter 6 Android SDK Versions and Compatibility (If you are curious, the data in this table is kept current at http://developer.android.com/ resources/dashboard/platform-versions.html. You can also find trend data there.) Why do so many devices still run older versions of Android? Most of it has to do with heavy competition among Android device manufacturers and US carriers. Carriers want features and phones that no other network has. Device manufacturers feel this pressure, too – all of their phones are based on the same OS, but they want to stand out from the competition. The combination of this pressure from the market and the carriers means that there is a bewildering array of devices with proprietary, one-off modifications of Android. A device with a proprietary version of Android is not able to run a new version of Android released by Google. Instead, it must wait for a compatible proprietary upgrade. That upgrade might not be available until months after Google releases its version, if it is ever available at all. Manufacturers often choose to spend resources on newer devices rather than keeping older ones up-to-date. And sometimes the hardware on an older device is simply not powerful enough to run a newer version of Android. Compatibility and Android Programming The delay in upgrades combined with regular new releases makes compatibility an important issue in Android programming. To reach a broad market, Android developers must create apps that perform well on devices running Froyo, Gingerbread, Honeycomb, Ice Cream Sandwich, and Jelly Bean versions of Android, as well as on different device form factors. Targeting different sizes of devices is easier than you might think. Phone screens are a variety of sizes, but the Android layout system does a good job at adapting. Tablets require more work, but in that case you can use configuration qualifiers to do the job (as you will see in Chapter 22). For Google TV (which also runs Android), though, the differences in UI are large enough that you would usually want a separate app. Versions are a different story. When releases are incremental, there is little problem with backwards compatibility. However, there has been one especially tectonic shift. Honeycomb was big The biggest issue in Android compatibility is navigating the divide between the pre- and postHoneycomb worlds. The release of Honeycomb was a major shift in Android and introduced a new UI and new architectural components. Honeycomb was just for tablets and Google TV (and not widely adopted), so it was not until Ice Cream Sandwich that these new developments were widely available. Since then, new releases have been more incremental. Given that more than half of devices still run Gingerbread or older devices, developers cannot make a clean break and forsake that which came before. Eventually older devices will become less prevalent, but this is happening much more slowly than you might imagine. Thus, Android developers must spend time ensuring backwards compatibility and bridging the gap between Gingerbread (API level 10) and Honeycomb (API level 11) and beyond. Android has provided help for maintaining backwards compatibility. There are also third-party libraries that can help. But maintaining compatibility does complicate learning Android programming. Often you will have to learn two ways of doing things as well as how to integrate those two ways. At other times, you will learn just one way that seems inordinately complicated because you are addressing at least two sets of requirements. 114 Honeycomb was big If you can swing it, we suggest going to sleep for a couple of years. Then you can wake up and learn Android programming once Gingerbread devices no longer make up a meaningful share of the market. If that is not possible, we hope it is at least helpful to understand the reason for some of the complications. When you created the GeoQuiz project, you set three SDK versions within the new application wizard. (Note that Android uses the terms “SDK version” and “API level” interchangeably.) Figure 6.1 Remember me? Let’s see where each of these settings lives in your project, explain the default choices, and see how to change them. The minimum required SDK version and the target SDK version are both set in the manifest. From the package explorer, reopen AndroidManifest.xml. Within the uses-sdk element find the values for android:minSdkVersion and android:targetSdkVersion. 115 Chapter 6 Android SDK Versions and Compatibility Listing 6.1 Finding minSdkVersion in the manifest (AndroidManifest.xml)Minimum SDK version Recall that the manifest is meta-data that the OS uses to interact with your app. The minSdkVersion value is a hard floor below which the OS should refuse to install the app. By setting this version to API level 8 (Froyo), you give Android permission to install GeoQuiz on devices running Froyo or higher. Android will refuse to install GeoQuiz on a device running, say, Eclair. Looking again at Table 6.1, you can see why Froyo is a good choice for a minimum SDK version: it allows your app to be installed on 95% of devices in use. Target SDK version The targetSdkVersion value tells Android which API level your app was designed to run on. Most often this will be the latest Android release. When would you lower the target SDK? New SDK releases can change how your app appears on a device or even how the OS behaves behind the scenes. If you have already designed an app, you should confirm that it works as expected on new releases. Check the documentation at http:// developer.android.com/reference/android/os/Build.VERSION_CODES.html to see where problems might arise. Then you can modify your app to work with the new behavior or lower the target SDK. Lowering the target SDK ensures that your app will still run with the appearance and behavior of the targeted version on which it worked well. Any changes in subsequent releases are ignored. Build SDK version The last SDK setting labeled Compile�With in Figure 6.1 is the build SDK version. This setting is not found in the manifest. Whereas the minimum and target SDK versions are advertised to the OS, the build SDK version is private information between you and the compiler. Android’s features are exposed through the classes and methods in the SDK. The build SDK version, or build target, specifies which version to use when building your own code. When Eclipse is looking to find the classes and methods you refer to in your imports, the build target determines which SDK version it checks against. The best choice for a build target is the latest API level (currently 17, Jelly Bean). However, you can change the build target of an existing application if you need to. For instance, you might want to update the build target when yet another version of Android is released. 116 Build SDK version To change the build target, right-click the GeoQuiz project in the package explorer and select Properties. From the lefthand side of the dialog, select Android and you will see all of your options for different build targets. Figure 6.2 Changing the build target Wondering about the difference between the Google APIs and Android Open Source Project targets? The Google APIs include the Android APIs plus Google’s additions – most importantly the APIs for using Google Maps. GeoQuiz’s build target is fine as it is, so click Cancel and let’s move on. 117 Chapter 6 Android SDK Versions and Compatibility Adding code from later APIs safely The difference between GeoQuiz’s minimum SDK version and build SDK version leave you with a compatibility gap to manage. For example, in GeoQuiz, what happens if you call code from an SDK version that is later than the minimum SDK of Froyo (API level 8)? When your app is installed and run on a Froyo device, it will crash. This used to be a testing nightmare. However, thanks to improvements in Android Lint, potential problems caused by calling newer code on older devices can be caught at runtime. If you use code from a higher version than your minimum SDK, Android Lint will report build errors. Right now, all of GeoQuiz’s simple code was introduced in API level 8 or earlier. Let’s add some code from API level 11 and see what happens. Open QuizActivity.java. In onCreate(Bundle), add the following code that creates a subtitle in the action bar that specifies what area of geographic knowledge you are testing. Listing 6.2 Adding action bar code (QuizActivity.java) protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); ActionBar actionBar = getActionBar(); actionBar.setSubtitle("Bodies of Water"); mIsCheater = false; (We are using a literal string here for simplicity’s sake. If you really wanted a subtitle or even different subtitles for different groups of quiz questions, then you would add one or more string resources and refer to them here.) Organize your imports to import the ActionBar class. The ActionBar class was introduced in API level 11, so this code would crash on a device running a lower version than that. You will see more of ActionBar in Chapter 16. Here we will just use it as an example of code that is unfamiliar to Froyo. After organizing your imports, select GeoQuiz in the package explorer and choose Android�Tools → Run�Lint:�Check�for�Common�Errors. Because your build SDK version is API level 17, the compiler itself has no problem with this code. Android Lint, on the other hand, knows about your minimum SDK version and will complain loudly. The error messages read something like Class�requires�API�level�11�(current�min�is�8). Basically, Android Lint will not let you build until you address this compatibility problem. How do you get rid of these errors? One option is to raise the minimum SDK version to 11. However, raising the minimum SDK version is not really dealing with this compatibility problem as much as ducking it. If your app cannot be installed on Gingerbread and older devices, then you no longer have a compatibility problem. A better option is to wrap the ActionBar code in a conditional statement that checks the device’s version of Android. 118 Adding code from later APIs safely Listing 6.3 Checking the device’s build version first @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { ActionBar actionBar = getActionBar(); actionBar.setSubtitle("Bodies of Water"); } The Build.VERSION.SDK_INT constant is the device’s version of Android. You then compare that version with the constant that stands for Honeycomb. (Version codes are listed at http:// developer.android.com/reference/android/os/Build.VERSION_CODES.html.) Now your ActionBar code will only be called when the app is running on a device with Honeycomb or higher. You have made your code safe for Froyo, and Android Lint should be content. However, if you try to run again, you will get the same error. Suppressing Lint compatibility errors Unfortunately, Android Lint cannot figure out what you have done here, so you have to suppress the errors explicitly. Add the following annotation before the implementation of onCreate(Bundle). Organize your imports to import android.annotation.TargetApi. Listing 6.4 Giving Android Lint a helpful clue @TargetApi(11) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate() called"); setContentView(R.layout.activity_quiz); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { ActionBar actionBar = getActionBar(); actionBar.setSubtitle("Bodies of Water"); } To understand why you need this annotation in addition to the if statement, imagine that Android programming is a beach. In the water at this beach, there are sharks – runtime exceptions from using newer methods or classes on older devices. Android Lint is a lifeguard on the beach. It is keeping a lookout, eager to jump in and save you if it looks like a shark is coming near. The code in Listing 6.4 does two things: it uses shark repellent, and it waves off the lifeguard. The if statement is your shark repellent. Wrapped in that if statement, the getActionBar() method will only be called when it is available, and the sharks will not attack you. The @TargetApi(11) annotation is waving off the lifeguard (Android Lint) saying, “Don’t worry about these sharks – I’ve got it under control.” Then the lifeguard will refrain from swooping down to pull you out of the water. So if you wave off the lifeguard with the @TargetApi annotation, please make sure you are wearing your SDK_INT shark repellent. Otherwise, you will be eaten by runtime exception sharks. 119 Chapter 6 Android SDK Versions and Compatibility Run GeoQuiz on a Honeycomb or higher device and check out your new subtitle. Figure 6.3 Action bar with subtitle You can also run GeoQuiz on a Froyo or Gingerbread device (virtual or otherwise). It will not have an action bar or a subtitle, but you can confirm that the app still runs safely. Using the Android Developer Documentation Android Lint errors will tell you what API level your incompatible code is from. But you can also find out which API level particular classes and methods belong to in Android’s developer documentation. It is good to get comfortable using the developer documentation right away. There is far too much in the Android SDKs to keep in your head, and, with new versions appearing regularly, you will need to learn what is new and how to use it. The Android developer documentation is an excellent and voluminous source of information. The main page of the documentation is http://developer.android.com/. It is split into three parts: Design, Develop, and Distribute. The Design section of the documentation includes patterns and principles for the UI design of your apps. The Develop section contains documentation and training. The Distribute section shows you how to prepare and publish your apps on Google Play or through open distribution. It is all worth perusing when you get a chance. The Develop section is further divided into four areas: Android�Training 120 beginning and advanced developer training modules including downloadable sample code Using the Android Developer Documentation API�Guides topic-based descriptions of app components, features, and best practices Reference searchable, linked documentation of every class, method, interface, attribute constant, etc. in the SDK Tools Descriptions and links to developer tools You do not have to be online to have access to the documentation. If you navigate on your filesystem to where you have downloaded the SDKs, there is a docs directory that contains the complete documentation. To determine what API level getActionBar() belongs to, search for this method using the search bar at the top-right of the browser. The first result will be an API Guide concerning the action bar. However, what you want are results from the reference section. Filter the search results by clicking Reference on the lefthand side. Select the first result, and you will be sent to the Activity class reference page. At the top of this page are links to its different sections. Click the Methods link to see a list of Activity’s methods. Figure 6.4 Activity reference page 121 Chapter 6 Android SDK Versions and Compatibility Scroll down, find the getActionBar() method, and click on the method name to see a description. To the right of the method signature, you can see that getActionBar() was introduced in API level 11. If you want to see which Activity methods are available in, say, API level 8, you can filter the reference by API level. On the lefthand side of the page where the classes are indexed by package, find where it says API�level:�17. Click the adjacent control and select 8 from the list. Everything that Android has introduced after API level 8 will be grayed-out. Figure 6.5 Activity methods filtered for API level 8 As you continue through this book, be sure to visit the documentation often. You will certainly need the documentation to tackle the challenge exercises, but also consider exploring whenever you get curious about particular classes, methods, or other topics. Android is always updating and improving the documentation, so there is always something new to learn. Challenge: Reporting the Build Version Add a TextView widget to the GeoQuiz layout that reports the API level that the device is running to the user. 122 Challenge: Reporting the Build Version Figure 6.6 Finished challenge You cannot set this TextView’s text in the layout because you will not know the device’s build version until runtime. Find the TextView method for setting text in the TextView reference page in Android’s documentation. You are looking for a method that accepts a single argument – a string (or a CharSequence). Use other XML attributes listed in the TextView reference to adjust the size or typeface of the text. 123 This page intentionally left blank 7 UI Fragments and the Fragment Manager In this chapter, you will start building an application named CriminalIntent. CriminalIntent records the details of “office crimes” – things like leaving dirty dishes in the breakroom sink or walking away from an empty shared printer after your documents have printed. With CriminalIntent, you can make a record of a crime including a title, a date, and a photo. You can also identify a suspect from your contacts and lodge a complaint via email, Twitter, Facebook, or other app. After documenting and reporting a crime, you can proceed with your work free of resentment and ready to focus on the business at hand. CriminalIntent is a complex app that will take thirteen chapters to complete. It will have a list-detail interface: The main screen will display a list of recorded crimes. Users will be able add new crimes or select an existing crime to view and edit its details. Figure 7.1 CriminalIntent, a list-detail app 125 Chapter 7 UI Fragments and the Fragment Manager The Need for UI Flexibility You might imagine that a list-detail application consists of two activities: one managing the list and the other managing the detail view. Clicking a crime in the list would start an instance of the detail activity. Pressing the Back button would destroy the detail activity and return you to the list where you could select another crime. That would work, but what if you wanted more sophisticated presentation and navigation between screens? • Imagine that your user is running CriminalIntent on a tablet. Tablets and some larger phones have screens large enough to show the list and detail at the same time – at least in landscape orientation. Figure 7.2 Ideal list-detail interface for phone and tablet • Imagine the user is viewing a crime on a phone and wants to see the next crime in the list. It would be better if the user could swipe to see the next crime without having to return to the list. Each swipe should update the detail view with information for the next crime. What these scenarios have in common is UI flexibility: the ability to compose and recompose an activity’s view at runtime depending on what the user or the device requires. Activities were not built to provide this flexibility. An activity’s views may change at runtime, but the code to control those views must live inside the activity. As a result, activities were tightly coupled to a particular screen used by the user. Introducing Fragments You can get around the letter of the law of Android by moving the app’s UI management from the activity to one or more fragments. A fragment is a controller object that an activity can deputize to perform tasks. Most commonly, the task is managing a user interface. The user interface can be an entire screen or just one part of the screen. A fragment managing a user interface is known as a UI fragment. A UI fragment has a view of its own that is inflated from a layout file. The fragment’s view contains the interesting UI elements that the user wants to see and interact with. 126 Starting CriminalIntent The activity’s view contains a spot where the fragment’s view will be inserted. Or it might have several spots for the views of several fragments. You can use the fragment(s) associated with the activity to compose and re-compose the screen as your app and users require. The activity’s view technically stays the same throughout its lifetime, and no laws of Android are violated. Let’s see how this would work in a list-detail application when displaying the list and detail together. You would compose the activity’s view from a list fragment and a detail fragment. The detail view would show the details of the selected list item. Selecting another item should display a new detail view. This is easy with fragments; the activity will replace the detail fragment with another detail fragment (Figure 7.3). No activities need to die for this major view change to happen. Figure 7.3 Detail fragment is swapped out Using UI fragments separates the UI of your app into building blocks, which is useful for more than just list-detail applications. Working with individual blocks, it is easy to build tab interfaces, tack on animated sidebars, and more. Achieving this UI flexibility comes at a cost: more complexity, more moving parts, and more code. You will reap the benefits of using fragments in Chapter 11 and Chapter 22. The complexity, however, starts now. Starting CriminalIntent In this chapter, you are going to start on the detail part of CriminalIntent. Figure 7.4 shows you what CriminalIntent will look like at the end of this chapter. It may not seem like a very exciting goal to shoot for. Just keep in mind that this chapter is about laying the foundation to do bigger things later. 127 Chapter 7 UI Fragments and the Fragment Manager Figure 7.4 CriminalIntent at the end of this chapter The screen shown in Figure 7.4 will be managed by a UI fragment named CrimeFragment. An instance of CrimeFragment will be hosted by an activity named CrimeActivity. For now, think of hosting as the activity providing a spot in its view hierarchy where the fragment can place its view (Figure 7.5). A fragment is incapable of getting a view on screen itself. Only when it is placed in an activity’s hierarchy will its view appear. Figure 7.5 CrimeActivity hosting a CrimeFragment 128 Starting CriminalIntent CriminalIntent will be a large project, and one way to keep your head wrapped around a project is with an object diagram. Figure 7.6 gives you the big picture of CriminalIntent. You do not have to memorize these objects and their relationships, but it is good to have an idea of where you are heading before you start. You can see that CrimeFragment will do the sort of work that your activities did in GeoQuiz: create and manage the user interface and interact with the model objects. Figure 7.6 Object diagram for CriminalIntent (for this chapter) Three of the classes shown in Figure 7.6 are classes that you will write: Crime, CrimeFragment, and CrimeActivity. An instance of Crime will represent a single office crime. In this chapter, a crime will have only a title and an ID. The title is a descriptive name, like “Toxic sink dump” or “Someone stole my yogurt!”. The ID will uniquely identify an instance of Crime. For this chapter, you will keep things very simple and use a single instance of Crime. CrimeFragment will have a member variable (mCrime) to hold this isolated incident. CrimeActivity’s view will appear. view will consist of a FrameLayout that defines the spot where the CrimeFragment’s CrimeFragment’s view will consist of a LinearLayout and an EditText. CrimeFragment will have a member variable for the EditText (mTitleField) and will set a listener on it to update the model layer when the text changes. 129 Chapter 7 UI Fragments and the Fragment Manager Creating a new project Enough talk; time to build a new app. Create a new Android application (New → Android�Application�Project). Name the application CriminalIntent and name the package com.bignerdranch.android.criminalintent, as shown in Figure 7.7. Build to the latest APIs and ensure that the app is compatible with devices running Froyo. Figure 7.7 Creating the CriminalIntent application In the next dialog, uncheck the box to create a custom launcher icon and click Next. After that, choose to create an activity using the blank activity template and click Next. Finally, name the activity CrimeActivity and click Finish (Figure 7.8). 130 Fragments and the support library Figure 7.8 Configuring CrimeActivity Fragments and the support library Fragments were introduced in API level 11 along with the first Android tablets and the sudden need for UI flexibility. You have created CriminalIntent with a minimum required SDK of 8, so you must find a way to ensure its compatibility with older versions of Android. Fortunately, in the case of fragments, ensuring backwards-compatibility is relatively painless: you simply use the fragment-related classes found in Android’s support library. The support library is already a part of your project. You can find the support library under libs/ android-support-v4.jar where it was added by the template. The support library includes a Fragment class (android.support.v4.app.Fragment) that you can use on any Android device running API level 4 or higher. When you use a support library class, it is not just used on older versions where no native class is available; it is also used on newer versions instead of the native class. Another important support library class is FragmentActivity (android.support.v4.app.FragmentActivity). Using fragments requires activities that know how to manage fragments. In Honeycomb and later versions of Android, all Activity subclasses know how to manage fragments. In earlier versions, Activity knows nothing about fragments – and neither will your Activity subclass. For compatibility with lower API levels, you subclass FragmentActivity instead. FragmentActivity is an Activity subclass that provides the fragment-managing capabilities of the newer Activity class, even on earlier Android versions. 131 Chapter 7 UI Fragments and the Fragment Manager Figure 7.9 Where the different fragment classes live Figure 7.9 shows you the name of each of these classes and where they live. Since the support library (and android.support.v4.app.Fragment) lives with your application, it is safe to use no matter where your app is running. In the package explorer, find and open CrimeActivity.java. Change CrimeActivity’s superclass to FragmentActivity. While you are there, remove the template’s implementation of onCreateOptionsMenu(Menu). (You will be creating an options menu for CriminalIntent from scratch in Chapter 16.) Listing 7.1 Tweaking template code (CrimeActivity.java) public class CrimeActivity extends Activity FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crime); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_crime, menu); return true; } Before proceeding further with CrimeActivity, let’s create the model layer for CriminalIntent by writing the Crime class. 132 Creating the Crime class Creating the Crime class In the package explorer, right-click the com.bignerdranch.android.criminalintent package and select New → Class. Name the class Crime, leave its superclass as java.lang.Object, and click Finish. In Crime.java, add the following code: Listing 7.2 Adding to Crime class (Crime.java) public class Crime { private UUID mId; private String mTitle; } public Crime() { // Generate unique identifier mId = UUID.randomUUID(); } Next, you want to generate only a getter for the read-only mId and both a getter and setter for mTitle. Right-click after the constructor and select Source → Generate�Getters�and�Setters. To generate only a getter for mId, click the arrow to the left of the variable name to reveal the possible methods and check only the box beside getId(), as shown in Figure 7.10. Figure 7.10 Generate two getters and one setter 133 Chapter 7 UI Fragments and the Fragment Manager Listing 7.3 Generated getters and setter (Crime.java) public class Crime { private UUID mId; private String mTitle; public Crime() { mId = UUID.randomUUID(); } public UUID getId() { return mId; } public String getTitle() { return mTitle; } public void setTitle(String title) { mTitle = title; } } That is all you need for the Crime class and for CriminalIntent’s model layer in this chapter. At this point, you have created the model layer and an activity that is capable of hosting a fragment in a way that is compatible with Froyo and Gingerbread versions. Now you will get into the details of how the activity performs its duties as host. Hosting a UI Fragment To host a UI fragment, an activity must • define a spot in its layout for the fragment’s view • manage the lifecycle of the fragment instance The fragment lifecycle Figure 7.11 shows the fragment lifecycle. It is similar to the activity lifecycle: it has stopped, paused, and running states, and it has methods you can override to get things done at critical points – many of which correspond to activity lifecycle methods. 134 Two approaches to hosting Figure 7.11 Fragment lifecycle diagram The correspondence is important. Because a fragment works on behalf of an activity, its state should reflect the activity’s state. Thus, it needs corresponding lifecycle methods to handle the activity’s work. One critical difference between the fragment lifecycle and the activity lifecycle is that fragment lifecycle methods are called by the hosting activity, not the OS. The OS knows nothing about the fragments that an activity is using to manage things. Fragments are the activity’s internal business. You will see more of the fragment lifecycle methods as you continue building CriminalIntent. Two approaches to hosting You have two options when it comes to hosting a UI fragment in an activity: • add the fragment to activity’s layout • add the fragment in the activity’s code The first approach is known as using a layout fragment. It is simple but inflexible. If you add the fragment to the activity’s layout, you hard-wire the fragment and its view to the activity’s view and cannot swap out that fragment during the activity’s lifetime. 135 Chapter 7 UI Fragments and the Fragment Manager Despite their inflexibility, layout fragments are useful, and you will see more of them in Chapter 13. The second approach is the more complex way to host, but it is the only way to have control at runtime over your fragments. You determine when the fragment is added to the activity and what happens to it after that. You can remove the fragment, replace it with another, and then add the first fragment back again. Thus, to achieve real UI flexibility you must add your fragment in code. This is the approach you will use for CrimeActivity’s hosting of a CrimeFragment. The code details will come later in the chapter. First, you are going to define CrimeActivity’s layout. Defining a container view You will be adding a UI fragment in the hosting activity’s code, but you still need to make a spot for the fragment’s view in the activity’s view hierarchy. In CrimeActivity’s layout, this spot will be the FrameLayout shown in Figure 7.12. Figure 7.12 Fragment-hosting layout for CrimeActivity This FrameLayout will be the container view for a CrimeFragment. Notice that the container view is completely generic; it does not name the CrimeFragment class. You can and will use this same layout to host other fragments. Locate CrimeActivity’s layout at res/layout/activity_crime.xml. Open this file and replace the default layout with the FrameLayout diagrammed in Figure 7.12. Your XML should match that in Listing 7.4. Listing 7.4 Create fragment container layout (activity_crime.xml) ... Note that, while activity_crime.xml consists solely of a container view for a single fragment, an activity’s layout can be more complex and define multiple container views as well as widgets of its own. You can preview your layout file or run CriminalIntent to check your code. You will only see an empty FrameLayout because the CrimeActivity is not yet hosting a fragment. 136 Creating a UI Fragment Figure 7.13 An empty FrameLayout Later, you will write code that puts a fragment’s view inside this FrameLayout. But first you need to create a fragment. Creating a UI Fragment The steps to creating a UI fragment are the same as those you followed to create an activity: • compose an interface by defining widgets in a layout file • create the class and set its view to be the layout that you defined • wire up the widgets inflated from the layout in code Defining CrimeFragment’s layout CrimeFragment’s view will display the information contained within an instance of Crime. Eventually, the Crime class and CrimeFragment’s view will include many interesting pieces, but for this chapter, you just need a text field to contain the crime’s title. Figure 7.14 shows the layout for CrimeFragment’s view. It consists of a vertical LinearLayout that contains an EditText. EditText is a widget that presents an area where the user can add or edit text. 137 Chapter 7 UI Fragments and the Fragment Manager Figure 7.14 Initial layout for CrimeFragment To create a layout file, right-click the res/layout folder in the package explorer and select New → Android�XML�File. Make sure that the resource type is Layout and name this file fragment_crime.xml. Select LinearLayout as the root element and click Finish. When the file opens, navigate to the XML. The wizard has added the LinearLayout for you. Using Figure 7.14 as a guide, make the necessary changes to fragment_crime.xml. You can use Listing 7.5 to check your work. Listing 7.5 Layout file for fragment’s view (fragment_crime.xml) Open res/values/strings.xml, add a crime_title_hint string resource and remove the unneeded hello_world and menu_settings string resources that the template created for you. Listing 7.6 Adding and deleting strings (res/values/strings.xml) Save your files. Deleting the menu_settings string caused an error in your project. To fix it, go to the package explorer and find the file res/menu/activity_crime.xml. This file is a definition of the menu 138 Creating the CrimeFragment class that the template created, and it references the menu_settings string. You will not be using this menu file for CriminalIntent, so you can simply delete it from the package explorer. Deleting the menu resource will cause Eclipse to rebuild. Your project should now be error-free. Switch to the graphical layout tool to see a preview of fragment_crime.xml. Creating the CrimeFragment class Right-click the com.bignerdranch.android.criminalintent package and select New → Class. Name the class CrimeFragment and then click the Browse button to set the superclass. In the window that appears, start typing Fragment. The wizard will suggest a few classes. Choose the android.support.v4.app.Fragment class, which is the Fragment class from the support library. Click OK. Figure 7.15 Choosing the support library’s Fragment class (If you are seeing multiple versions of android.support.v4.app.Fragment, make sure to select the one that is part of the CriminalIntent project found in CriminalIntent/libs/.) Implementing fragment lifecycle methods is a controller that interacts with model and view objects. Its job is to present the details of a specific crime and update those details as the user changes them. CrimeFragment In the GeoQuiz app, your activities did most of their controller work in activity lifecycle methods. In CriminalIntent, this work will be done by fragments in fragment lifecycle methods. Many of these methods correspond to the Activity methods you already know, such as onCreate(Bundle). 139 Chapter 7 UI Fragments and the Fragment Manager In CrimeFragment.java, add a member variable for the Crime instance and an implementation of Fragment.onCreate(Bundle). Listing 7.7 Overriding Fragment.onCreate(Bundle) (CrimeFragment.java) public class CrimeFragment extends Fragment { private Crime mCrime; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCrime = new Crime(); } } There are a couple of things to notice in this implementation. First, Fragment.onCreate(Bundle) is a public method whereas Activity.onCreate(Bundle) is protected. Fragment.onCreate(…) and other Fragment lifecycle methods must be public because they will be called by whatever activity is hosting the fragment. Second, similar to an activity, a fragment has a bundle to which it saves and retrieves its state. You can override Fragment.onSaveInstanceState(Bundle) for your own purposes just like with Activity.onSaveInstanceState(Bundle). Note what does not happen in Fragment.onCreate(…): you do not inflate the fragment’s view. You configure the fragment instance in Fragment.onCreate(…), but you create and configure the fragment’s view in another fragment lifecycle method: public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) This method is where you inflate the layout for the fragment’s view and return the inflated View to the hosting activity. The LayoutInflater and ViewGroup parameters are necessary to inflate the layout. The Bundle will contain data that this method can use to recreate the view from a saved state. In CrimeFragment.java, add an implementation of onCreateView(…) that inflates fragment_crime.xml. Listing 7.8 Overriding onCreateView(…) (CrimeFragment.java) public class CrimeFragment extends Fragment { private Crime mCrime; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCrime = new Crime(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_crime, parent, false); return v; } } 140 Creating the CrimeFragment class Within onCreateView(…), you explicitly inflate the fragment’s view by calling LayoutInflater.inflate(…) and passing in the layout resource ID. The second parameter is your view’s parent, which is usually needed to configure the widgets properly. The third parameter tells the layout inflater whether to add the inflated view to the view’s parent. You pass in false because you will add the view in the activity’s code. Wiring widgets in a fragment The onCreateView(…) method is also the place to wire up the EditText to respond to user input. After the view is inflated, get a reference to the EditText and add a listener. Listing 7.9 Wiring up the EditText widget (CrimeFragment.java) public class CrimeFragment extends Fragment { private Crime mCrime; private EditText mTitleField; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_crime, parent, false); mTitleField = (EditText)v.findViewById(R.id.crime_title); mTitleField.addTextChangedListener(new TextWatcher() { public void onTextChanged( CharSequence c, int start, int before, int count) { mCrime.setTitle(c.toString()); } public void beforeTextChanged( CharSequence c, int start, int count, int after) { // This space intentionally left blank } }); } } public void afterTextChanged(Editable c) { // This one too } return v; Getting references in Fragment.onCreateView(…) works nearly the same as in Activity.onCreate(…). The only difference is that you call View.findViewById(int) on the fragment’s view. The Activity.findViewById(int) method that you used before is a convenience method that calls View.findViewById(int) behind the scenes. The Fragment class does not have a corresponding convenience method, so you have to call the real thing. Setting listeners in a fragment works exactly the same as in an activity. In Listing 7.9, you create an anonymous class that implements the TextWatcher listener interface. TextWatcher has three methods, but you only care about one: onTextChanged(…). 141 Chapter 7 UI Fragments and the Fragment Manager In onTextChanged(…), you call toString() on the CharSequence that is the user’s input. This method returns a string, which you then use to set the Crime’s title. Your code for CrimeFragment is now complete. It would be great if you could run CriminalIntent now and play with the code you have written. But you cannot. Fragments cannot put their views on screen. To realize your efforts, you first have to add a CrimeFragment to CrimeActivity. Adding a UI Fragment to the FragmentManager When the Fragment class was introduced in Honeycomb, the Activity class was changed to include a piece called the FragmentManager. The FragmentManager is responsible for managing your fragments and adding their views to the activity’s view hierarchy. The FragmentManager handles two things: a list of fragments and a back stack of fragment transactions (which you will learn about shortly). Figure 7.16 The FragmentManager For CriminalIntent, you will only be concerned with the FragmentManager’s list of fragments. To add a fragment to an activity in code, you make explicit calls to the activity’s FragmentManager. The first step is to get the FragmentManager itself. In CrimeActivity.java, add the following code to onCreate(…). Listing 7.10 Get the FragmentManager (CrimeActivity.java) public class CrimeActivity extends FragmentActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crime); } 142 } FragmentManager fm = getSupportFragmentManager(); Fragment transactions You call getSupportFragmentManager() because you are using the support library and the FragmentActivity class. If you were not interested in compatibility with pre-Honeycomb devices, then you would subclass Activity and call getFragmentManager(). Fragment transactions Now that you have the FragmentManager, add the following code to give it a fragment to manage. (We will step through this code afterwards. Just get it in for now.) Listing 7.11 Adding a CrimeFragment (CrimeActivity.java) public class CrimeActivity extends FragmentActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crime); FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); } } if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } The best place to start understanding the code that you added in Listing 7.11 is not at the beginning. Instead, find the add(…) operation and the code around it. This code creates and commits a fragment transaction. Listing 7.12 A fragment transaction (CrimeActivity.java) if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); Fragment transactions are used to add, remove, attach, detach, or replace fragments in the fragment list. They are the heart of how you use fragments to compose and recompose screens at runtime. The FragmentManager maintains a back stack of fragment transactions that you can navigate. The FragmentManager.beginTransaction() method creates and returns an instance of FragmentTransaction. The FragmentTransaction class uses a fluent interface - methods that configure FragmentTransaction return a FragmentTransaction instead of void, which allows you to chain them together. So the highlighted code in Listing 7.12 says, “Create a new fragment transaction, include one add operation in it, and then commit it.” 143 Chapter 7 UI Fragments and the Fragment Manager The add(…) method is the meat of the transaction. It has two parameters: a container view ID and the newly-created CrimeFragment. The container view ID should look familiar. It is the resource ID of the FrameLayout that you defined in activity_crime.xml. A container view ID serves two purposes: • It tells the FragmentManager where in the activity’s view the fragment’s view should appear. • It is used as a unique identifier for a fragment in the FragmentManager’s list. When you need to retrieve the CrimeFragment from the FragmentManager, you ask for it by container view ID: Listing 7.13 Existing fragment retrieved by container view ID (CrimeActivity.java) FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } It may seem odd that the FragmentManager identifies the CrimeFragment using the resource ID of a FrameLayout. But identifying a UI fragment by the resource ID of its container view is built into how the FragmentManager operates. Now we can summarize the code you added back in Listing 7.11 from start to finish. First, you ask the FragmentManager for the fragment with a container view ID of R.id.fragmentContainer. If this fragment is already in the list, the FragmentManager will return it. Why would a fragment already be in the list? The call to CrimeActivity.onCreate(…) could be in response to CrimeActivity being recreated after being destroyed on rotation or to reclaim memory. When an activity is destroyed, its FragmentManager saves out its list of fragments. When the activity is recreated, the new FragmentManager retrieves the list and recreates the listed fragments to make everything as it was before. On the other hand, if there is no fragment with the given container view ID, then fragment will be null. In this case, you create a new CrimeFragment and a new fragment transaction that adds the fragment to the list. is now hosting a CrimeFragment. Run CriminalIntent to prove it. You should see the view defined in fragment_crime.xml, as shown in Figure 7.17. CrimeActivity 144 The FragmentManager and the fragment lifecycle Figure 7.17 CrimeFragment’s view hosted by CrimeActivity A single widget on screen may not seem like a much of a reward for all the work you have done in this chapter. But what you have done is a lay a solid foundation to do greater things with CriminalIntent in the chapters ahead. The FragmentManager and the fragment lifecycle Now that you know about the FragmentManager, let’s take another look at the fragment lifecycle. 145 Chapter 7 UI Fragments and the Fragment Manager Figure 7.18 The fragment lifecycle, again The FragmentManager of an activity is responsible for calling the lifecycle methods of the fragments in its list. The onAttach(Activity), onCreate(Bundle), and onCreateView(…) methods are called when you add the fragment to the FragmentManager. The onActivityCreated(…) method is called after the hosting activity’s onCreate(…) method has executed. You are adding the CrimeFragment in CrimeActivity.onCreate(…), so this method will be called after the fragment has been added. What happens if you add a fragment while the activity is already stopped, paused, or running? In that case, the FragmentManager immediately walks the fragment through whatever steps are necessary to get it caught up to the activity’s state. For example, when you add a fragment to an activity that is already running, that fragment gets calls to onAttach(Activity), onCreate(Bundle), onCreateView(…), onActivityCreated(Bundle), onStart(), and then onResume(). Once the fragment’s state is caught up to the activity’s state, the hosting activity’s FragmentManager will call further lifecycle methods around the same time it receives the corresponding calls from the OS to keep the fragment’s state aligned with that of the activity. There is no guarantee about whether the fragment’s methods will be called before or after the activity’s methods. When you are using the support library, there is one small difference in the fragment lifecycle: If you add a fragment in Activity.onCreate(…), then onActivityCreated(…) is not called immediately 146 The Reason All Our Activities Will Use Fragments after Activity.onCreate(…). Instead, it is called as Activity.onStart() is being performed. Why? It is impossible to call onActivityCreated(…) at the right time from FragmentActivity in preHoneycomb SDKs, so it is called as soon as the next lifecycle method occurs. In practice, this usually makes no difference; onStart() occurs immediately after Activity.onCreate(…) anyway. The Reason All Our Activities Will Use Fragments From here on, all of the apps in this book will use fragments – no matter how simple. This may seem like overkill. Many of the examples you will see in following chapters could be written without fragments. The user interfaces could be created and managed from activities, and doing so might even be less code. However, we believe it is better for you to become comfortable with the pattern you will most likely use in real life. You might think it would be better to begin a simple app without fragments and add them later, when (or if) necessary. There is an idea in Extreme Programming methodology called YAGNI. YAGNI stands for “You Aren’t Gonna Need It,” and it urges you not to write code if you think you might need it later. Why? Because YAGNI. It is tempting to say “YAGNI” to fragments. Unfortunately, adding fragments later can be a minefield. Changing an activity to an activity hosting a UI fragment is not difficult, but there are swarms of annoying gotchas. Keeping some interfaces managed by activities and having others managed by fragments only makes things worse because you have to keep track of this meaningless distinction. It is far easier to write your code using fragments from the beginning and not worry about the pain and annoyance of reworking it later, or having to remember which style of controller you are using in each part of your application. Therefore, when it comes to fragments, we have a different principle: AUF, or “Always Use Fragments.” You can kill a lot of brain cells deciding whether to use a fragment or an activity, and it is just not worth it. AUF! For the More Curious: Developing for Honeycomb, ICS, Jelly Bean, and Beyond In this chapter, you learned how to use the support library to enable fragments for a project with a minimum SDK less than API level 11. However, if you were developing exclusively for newer SDKs, then you would not need to use the support library. Instead, you could use the native fragment classes in the standard library. To use standard library fragments, you would make four changes to the project: • Set the application’s build target and minimum SDK to API level 11 or higher. • Subclass the standard library Activity class (android.app.Activity) instead of FragmentActivity. Activities have support for fragments out of the box on API level 11 or higher. • Subclass android.app.Fragment instead of android.support.v4.app.Fragment. • To get the FragmentManager, call getFragmentManager() instead of getSupportFragmentManager(). 147 This page intentionally left blank 8 Creating User Interfaces with Layouts and Widgets In this chapter, you will learn more about layouts and widgets while adding a crime’s date and status to CriminalIntent. Upgrading Crime Open Crime.java and add two new fields. The Date field represents the date a crime occurred. The boolean field represents whether the crime has been solved. Listing 8.1 Adding more fields to Crime (Crime.java) public class Crime { private UUID mId; private String mTitle; private Date mDate; private boolean mSolved; public Crime() { mId = UUID.randomUUID(); mDate = new Date(); } } ... Initializing the Date variable using the default Date constructor sets mDate to the current date. This will be the default date for a crime. Next, generate getters and setters for your new fields (Source → Generate�Getters�and�Setters...). 149 Chapter 8 Creating User Interfaces with Layouts and Widgets Listing 8.2 Generated getters and setters (Crime.java) public class Crime { ... public void setTitle(String title) { mTitle = title; } public Date getDate() { return mDate; } public void setDate(Date date) { mDate = date; } } public boolean isSolved() { return mSolved; } public void setSolved(boolean solved) { mSolved = solved; } Your next steps are updating the layout in fragment_crime.xml with new widgets and wiring up those widgets in CrimeFragment.java. Updating the Layout Here is what CrimeFragment’s view will look like by the end of this chapter. Figure 8.1 CriminalIntent, episode 2 150 Updating the Layout To get this on screen, you are going to add four widgets to CrimeFragment’s layout: two TextView widgets, a Button, and a CheckBox. Open fragment_crime.xml and make the changes shown in Listing 8.3. You may get errors from missing string resources that you will create in a moment. Listing 8.3 Adding new widgets (fragment_crime.xml) CriminalIntent Hello world! Settings CrimeActivity Enter a title for the crime. Notice that you did not give the Button an android:text attribute. This button will display the date of the Crime being displayed, and its text will be set in code. Why display the date on a Button? You are preparing for the future. For now, a crime’s date defaults to the current date and cannot be changed. In Chapter 12, you will wire up the button so that a click presents a DatePicker widget from which the user can set the date to something else. There are some new things in this layout to discuss, such as the style attribute and the margin attributes. But first let’s get CriminalIntent up and running with the new widgets. Return to res/values/strings.xml and add the necessary string resources. 151 Chapter 8 Creating User Interfaces with Layouts and Widgets Listing 8.4 Adding string resources (strings.xml) Save your files and check for typos. Wiring Widgets The CheckBox needs to display whether a Crime has been solved. You also need to update the Crime’s mSolved field when a user toggles the CheckBox. For now, all the new Button needs to do is display the date in the Crime’s mDate field. In CrimeFragment.java, add two new instance variables. Listing 8.5 Adding widget instance variables (CrimeFragment.java) public class CrimeFragment extends Fragment { private Crime mCrime; private EditText mTitleField; private Button mDateButton; private CheckBox mSolvedCheckBox; @Override public void onCreate(Bundle savedInstanceState) { ... Organize your imports to resolve the reference to CheckBox. Next, in onCreateView(…), get a reference to the new button, set its text as the date of the crime, and disable it for now. Listing 8.6 Setting Button text (CrimeFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_crime, parent, false); ... mTitleField.addTextChangedListener(new TextWatcher() { ... }); mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setText(mCrime.getDate().toString()); mDateButton.setEnabled(false); } 152 return v; More on XML Layout Attributes Disabling the button ensures that it will not respond in any way to the user pressing it. It also changes its appearance to advertise its disabled state. In Chapter 12, you will enable the button when you set its listener. Moving on to the CheckBox, get a reference and set a listener that will update the mSolved field of the Crime. Listing 8.7 Listening for CheckBox changes (CrimeFragment.java) ... mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setText(mCrime.getDate().toString()); mDateButton.setEnabled(false); mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved); mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Set the crime's solved property mCrime.setSolved(isChecked); } }); } return v; When you import the OnCheckedChangeListener interface, Eclipse will offer you a choice between an interface defined in the CompoundButton class and one defined in the RadioGroup class. Choose the CompoundButton interface; CheckBox is a subclass of CompoundButton. If you used auto-complete, you may also see an @Override annotation above your onCheckedChanged(…) method that Listing 8.7 does not have. You can ignore this difference. @Overrides are not required for methods defined in interfaces. Run CriminalIntent. Toggle the new CheckBox and admire your disabled Button that displays the date. More on XML Layout Attributes Let’s go back over some of the attributes you added in fragment_crime.xml and answer some lingering questions you might have about widgets and attributes. Styles, themes, and theme attributes A style is an XML resource that contains attributes that describe how a widget should look and behave. For example, the following is a style resource that configures a widget with a larger-than-normal text size. You can create your own styles (and you will in Chapter 24). You add them to a styles file in res/ values/ and refer to them in layouts like this: @style/my_own_style. 153 Chapter 8 Creating User Interfaces with Layouts and Widgets Take another look at the TextView widgets in fragment_crime.xml; each has a style attribute that refers to a style created by Android. This particular style makes the TextViews look like list separators and comes from the app’s theme. A theme is a collection of styles. Structurally, a theme is itself a style resource whose attributes point to other style resources. Android provides platform themes that your apps can use. When you created CriminalIntent, the wizard suggested Holo�Light�with�Dark�Action�Bar as the app’s theme and you accepted. You can apply a style from the app’s theme to a widget using a theme attribute reference. This is what you are doing in fragment_crime.xml when you use the value ?android:listSeparatorTextViewStyle. In a theme attribute reference, you tell Android’s runtime resource manager, “Go to the app’s theme and find the attribute named listSeparatorTextViewStyle. This attribute points to another style resource. Put the value of that resource here.” Every Android theme will include an attribute named listSeparatorTextViewStyle, but its definition will be different depending on the overall look and feel of the particular theme. Using a theme attribute reference ensures that the TextViews will have the correct look and feel for your app. You will learn more about how styles and themes work in Chapter 24. Screen pixel densities and dp and sp In fragment_crime.xml, you specify the margin attribute values in terms of dp units. You have seen these units in layouts before; now it is time to learn what they are. Sometimes you need to specify values for view attributes in terms of specific sizes (usually in pixels but sometimes points, millimeters, or inches). You see this most commonly with attributes for text size, margins, and padding. Text size is the pixel height of the text on the device’s screen. Margins specify the distances between views, and padding specifies the distance between a view’s outside edges and its content. Android automatically scales images to different screen pixel densities using the drawable-ldpi, drawable-mdpi, and drawable-hdpi directories. But what happens when your images scale, but your margins do not? Or what happens when the user configures a larger-than-default text size? To solve these problems, Android provides density-independent dimension units that you can use to get the same size on different screen densities. Android translates these units into pixels at runtime, so there is no tricky math for you to do. 154 Android’s design guidelines Figure 8.2 Dimension units in action on TextView (left: MDPI; middle: HDPI; right: HDPI with large text) dp (or dip) Short for density-independent pixel and usually pronounced “dip.” You typically use this for margins, padding, or anything else for which you would otherwise specify size with a pixel value. When your display is a higher density, densityindependent pixels will expand to fill a larger number of screen pixels. One dp is always 1/160th of an inch on a device’s screen. You get the same size regardless of screen density. sp Short for scale-independent pixel. Scale-independent pixels are densityindependent pixels that also take into account the user’s font size preference. You will almost always use sp to set display text size. pt, mm, in These are scaled units like dp that allow you to specify interface sizes in points (1/72 of an inch), millimeters, or inches. However, we do not recommend using them: not all devices are correctly configured for these units to scale correctly. In practice and in this book, you will use dp and sp almost exclusively. Android will translate these values into pixels at runtime. Android’s design guidelines Notice that for your margins, you use a 16dp value for the margins in Listing 8.3. This value follows Android’s “48dp rhythm” design guideline. You can find all of Android design guidelines at http:// developer.android.com/design/index.html. Modern Android apps should follow these guidelines as closely as possible. The guidelines rely heavily on newer Android SDK functionality that is not always available or easy to achieve on older devices. Some of the recommendations can be followed using the support library, but many require the use of a third-party library, like ActionBarSherlock, which you can read about in Chapter 16. 155 Chapter 8 Creating User Interfaces with Layouts and Widgets Layout parameters By now, you have probably noticed that some attribute names begin with layout_ (android:layout_marginLeft) and others do not (android:text). Attributes whose names do not begin with layout_ are directions to the widget. When it is inflated, the widget calls a method to configure itself based on each of these attributes and their values. When an attribute’s name begins with layout_, that attribute is a direction to that widget’s parent. These attributes are known as layout parameters, and they tell the parent layout how to arrange the child element within the parent. Even when a layout object like LinearLayout is the root element of a layout, it is still a widget with a parent and has layout parameters. When you defined the LinearLayout in fragment_crime.xml, you gave it attributes for android:layout_width and android:layout_height. These attributes will be used by the LinearLayout’s parent layout when it is inflated. In this case, the LinearLayout’s layout parameters will be used by the FrameLayout in CrimeActivity’s content view. Margins vs. padding In fragment_crime.xml, you have given widgets margin and padding attributes. Beginning developers sometimes get confused between these attributes. Now that you understand what a layout parameter is, the difference is easier to explain. Margin attributes are layout parameters. They determine the distance between widgets. Given that a widget can only know about itself, margins must be the responsibility of the widget’s parent. Padding, on the other hand, is not a layout parameter. The android:padding attribute tells the widget how much bigger than its contents it should draw itself. For example, say you wanted the date button to be spectacularly large without changing its text size. You could add the following attribute to the Button, save your layout, and run again. Listing 8.8 Padding in action (fragment_crime.xml) 156 Using the Graphical Layout Tool Figure 8.3 I like big buttons and I cannot lie... Alas, you should probably remove this attribute before continuing. Using the Graphical Layout Tool So far, you have created layouts by typing XML. In this section, you will use the graphical layout tool. In particular, you are going to make an alternative landscape layout for CrimeFragment. Most built-in layout classes, like LinearLayout, will automatically stretch and resize themselves and their children on rotation. Sometimes, however, the default resizing does not make the best use of the available space. Run CriminalIntent and rotate the device to see the CrimeFragment layout in landscape orientation. Figure 8.4 CrimeFragment in landscape mode 157 Chapter 8 Creating User Interfaces with Layouts and Widgets The date button becomes oddly long; it would be better if the landscape layout had the button and check box side by side. To make this happen, create the res/layout-land directory (right-click res/ in the package explorer and select New → Folder). Then copy fragment_crime.xml into res/layoutland/. To make these changes using the graphical layout tool, first close res/layout/fragment_crime.xml if you have it open in the editor. Then open res/layout-land/fragment_crime.xml and select the Graphical�Layout tab. In the middle of the graphical layout tool is the preview you have already seen. On the lefthand side is the palette. This view contains all the widgets you could wish for, organized by category. Figure 8.5 Views in the graphical layout tool The outline view is to the right of the preview. The outline shows how the widgets are organized in the layout. Beneath the outline view is the properties view. In this view, you can view and edit the attributes of the widget selected in the outline. Now let’s consider what changes to make to this layout. Take a look at Figure 8.6. 158 Adding a new widget Figure 8.6 Landscape layout for CrimeFragment The changes can be broken into four parts: • add a new LinearLayout widget to the layout • edit the attributes of the LinearLayout • make the Button and CheckBox widgets children of the LinearLayout • update the layout parameters of the Button and CheckBox Adding a new widget You can add a widget by selecting it in the palette and then dragging to the outline view. Click the Layouts category in the palette. Select LinearLayout�(Horizontal) and drag to the outline view. Drop this LinearLayout on top of the root LinearLayout to add it as a direct child of the root LinearLayout. 159 Chapter 8 Creating User Interfaces with Layouts and Widgets Figure 8.7 LinearLayout added to fragment_crime.xml You can add widgets by dragging from the palette to the preview, too. However, layout widgets are often empty or obscured by other views, so it can be hard to see exactly where to drop a widget in the preview to get the hierarchy you want. Dragging to the outline view makes this much easier. Editing attributes in properties Select the new LinearLayout in the outline view to display its attributes in the properties view. Expand the Layout�Parameters category and then the Margins category. You want the new LinearLayout’s margins to match your other widgets. Select the field next to Left and type 16dp. Do the same for the right margin. Figure 8.8 Margins set in properties view (On some platforms, you will get a pop-up window when attempting to type in these fields. This popup is a workaround for a known bug. Unfortunately, the workaround will not allow you to enter a value. So, if this happens, save your file and switch to the XML. Then copy the two margin attributes from the EditText to the LinearLayout.) 160 Reorganizing widgets in the outline view Save your layout file and switch to the XML by selecting the fragment_crime.xml tab at the bottom of the preview. You should see a LinearLayout element with the margin attributes you just added. Reorganizing widgets in the outline view The next step is to make the Button and CheckBox children of the new LinearLayout. Return to the graphical layout tool, and, in the outline view, select the Button and drag it on top of the LinearLayout. The outline should reflect that the Button is now a child of the new LinearLayout. Do the same for the CheckBox. Figure 8.9 Button and CheckBox now children of new LinearLayout If widget children are out of order, you can reorder them in the outline view by dragging. You can also delete widgets from the layout in the outline view, but be careful: deleting a widget also deletes its children. Back in the preview, the CheckBox seems to be missing. The Button is obscuring it. The LinearLayout considered the width (match_parent) of its first child (the Button) and gave the first child all of the space leaving nothing for the CheckBox (Figure 8.10). 161 Chapter 8 Creating User Interfaces with Layouts and Widgets Figure 8.10 The first-defined Button child obscures the CheckBox You can introduce some equity in the LinearLayout’s parenting by adjusting the layout parameters of its children. Updating child layout parameters First, select the date button in the outline. In the properties view, click on the current Width value and change it to wrap_content. 162 Updating child layout parameters Figure 8.11 Changing Button’s width to wrap_content Next, delete both of the button’s 16dp margin values. The button will not need these margins now that it is inside the LinearLayout. Finally, find the Weight field in the Layout�Parameters section and set its value to 1. This field corresponds to the android:layout_weight attribute shown in Figure 8.6. Select the CheckBox in the outline view and make the same attribute changes: Width should be wrap_content, Weight should be 1, and the margins should be empty. Check the preview to confirm that both widgets are now visible. Then save your file and return to the XML to confirm your changes. Listing 8.9 shows the relevant XML. 163 Chapter 8 Creating User Interfaces with Layouts and Widgets Listing 8.9 XML for the graphically-created layout (layout-land/ fragment_crime.xml) ... CriminalIntent CrimeActivity Enter a title for this crime. Title Details Solved? How android:layout_weight works The android:layout_weight attribute tells the LinearLayout how to distribute its children. You have given both widgets the same value, but that does not necessarily make them the same width on screen. To determine the width of its child views, LinearLayout uses a mixture of the layout_width and layout_weight parameters. LinearLayout makes two passes to set the width of a view. In the first pass, LinearLayout looks at layout_width (or layout_height, for vertical orientation). The value for layout_width for both the Button and CheckBox is now wrap_content, so each view will get only enough space to draw itself (Figure 8.12). (It is hard to see how layout weights work in the preview because your button’s contents are not part of the layout itself. The following figures show what the LinearLayout would look like if the button already had its contents.) Figure 8.12 Pass 1: space given out based on layout_width In the next pass, LinearLayout allocates any extra space based on the values for layout_weight (Figure 8.13). 164 Summary of graphical layout tool Figure 8.13 Pass 2: extra space given out based on 1:1 layout_weight In your layout, the Button and CheckBox have the same value for layout_weight, so they split the extra space 50/50. If you set the weight for your Button to 2, then it would receive 2/3 of the extra space, leaving 1/3 for the CheckBox (Figure 8.14). Figure 8.14 Extra space divided unevenly based on 2:1 layout_weight Any floating point number can be a valid weight. Programmers have different conventions for the kinds of weights they use. In fragment_crime.xml, you are using a “cocktail recipe” style weighting. Another common convention is to have weights add up to 1.0 or 100, which would make the weight for the button in the last example 0.66 or 66, respectively. What if you want the LinearLayout to allocate exactly 50% of its width to each view? You simply skip the first pass by setting the layout_width of each widget to 0dp instead of wrap_content. This leaves layout_weight the sole component in the LinearLayout’s decision-making (Figure 8.15). Figure 8.15 When layout_width="0dp", only layout_weight values matter Summary of graphical layout tool The graphical layout tool is useful, and Android is improving it with every ADT release. However, it can still be slow and buggy at times. Sometimes, you may want to go back to typing XML. You can switch between making changes in the graphical layout tool and in XML. Just be sure to save the file before switching tabs to be safe. Feel free to use the graphical layout tool to create layouts in this book. From now on, we will show you a diagram like Figure 8.6 when you need to create a layout. You can decide how to create it – XML, graphical layout tool, or a little of both. Widget IDs and multiple layouts The two layouts that you have created for CriminalIntent do not vary significantly, but there may be times when your layouts will. When this is the case, you should ensure that widgets actually exist before you access them in code. 165 Chapter 8 Creating User Interfaces with Layouts and Widgets If you have a widget in one layout and not another, use null-checking in the code to determine if the widget is present in the current orientation before calling methods on it: } Button landscapeOnlyButton = (Button)v.findViewById(R.id.landscapeOnlyButton); if (landscapeOnlyButton != null) { // Set it up Finally, remember that a widget must have the same android:id attribute in every layout in which it appears so that your code can find it. Challenge: Formatting the Date The Date object is more of a timestamp than a conventional date. A timestamp is what you see when you call toString() on a Date, so that is what you have on your button. While timestamps make for good documentation, it might be nicer if the button just displayed the date as humans think of it – like “Oct 12, 2012.” You can do this with an instance of the android.text.format.DateFormat class. The place to start is the reference page for this class in the Android documentation. You can use methods in the DateFormat class to get a common format. Or you can prepare your own format string. For a more advanced challenge, create a format string that will display the day of the week as well – for example, “Tuesday, Oct 12, 2012.” 166 9 Displaying Lists with ListFragment CriminalIntent’s model layer currently consists of a single instance of Crime. In this chapter, you will update CriminalIntent to work with a list of crimes. The list will display each Crime’s title and date and whether the case has been solved. Figure 9.1 A list of crimes Figure 9.2 shows the overall plan for CriminalIntent in this chapter. 167 Chapter 9 Displaying Lists with ListFragment Figure 9.2 CriminalIntent with a list of crimes In the model layer, you have a new object, CrimeLab, that will be a centralized data stash that stores Crime objects. Displaying a list of crimes requires a new activity and a new fragment in CriminalIntent’s controller layer: CrimeListActivity and CrimeListFragment. will be a subclass of ListFragment, which is a subclass of Fragment that comes with built-in help for displaying lists. These controllers will interact with each other and CrimeLab to access the data in the model layer. CrimeListFragment (Where are CrimeActivity and CrimeFragment in Figure 9.2? They are part of the detail view, so we are not showing them here. In Chapter 10, you will connect the list and the detail parts of CriminalIntent.) In Figure 9.2, you can also see the view objects associated with CrimeListActivity and CrimeListFragment. The activity’s view will consist of a fragment-containing FrameLayout. The fragment’s view will consist of a ListView. You will learn more about how ListFragment and ListView interact later in the chapter. Updating CriminalIntent’s Model Layer The first step is to upgrade CriminalIntent’s model layer from a single Crime object to an ArrayList of Crime objects. is a Java class that supports an ordered list of objects of a given type. It provides methods for retrieving, adding, and deleting elements. ArrayList 168 Singletons and centralized data storage Singletons and centralized data storage You are going to store the array list of crimes in a singleton. A singleton is a class that allows only one instance of itself to be created. A singleton exists as long as the application stays in memory, so storing the list in a singleton will keep the crime data available no matter what happens with activities, fragments, and their lifecycles. To create a singleton, you create a class with a private constructor and a get() method that returns the instance. If the instance already exists, then get() simply returns the instance. If the instance does not exist yet, then get() will call the constructor to create it. Right-click the com.bignerdranch.android.criminalintent package and choose New → Class. Name this class CrimeLab and then click Finish. In CrimeLab.java, implement CrimeLab as a singleton with a private constructor and a get(Context) method. Listing 9.1 Setting up the singleton (CrimeLab.java) public class CrimeLab { private static CrimeLab sCrimeLab; private Context mAppContext; private CrimeLab(Context appContext) { mAppContext = appContext; } } public static CrimeLab get(Context c) { if (sCrimeLab == null) { sCrimeLab = new CrimeLab(c.getApplicationContext()); } return sCrimeLab; } Notice the s prefix on the sCrimeLab variable. You are using this Android convention to make it clear that sCrimeLab is a static variable. The CrimeLab constructor requires a Context parameter. This is common in Android; having a Context parameter allows the singleton to start activities, access project resources, find your application’s private storage, and more. Notice in get(Context) that you do not directly pass in the Context parameter to the constructor. This Context could be an Activity or another Context object, like Service. You cannot be sure that just any Context will exist as long as CrimeLab needs it, which is for the life of the application. To ensure that your singleton has a long-term Context to work with, you call getApplicationContext() and trade the passed-in Context for the application context. The application context is a Context that is global to your application. Whenever you have an applicationwide singleton, it should always use the application context. Let’s give CrimeLab some Crime objects to store. In CrimeLab’s constructor, create an empty ArrayList of Crimes. Also add two methods: a getCrimes() method that returns the array list and a getCrime(UUID) that returns the Crime with the given ID (Listing 9.2). 169 Chapter 9 Displaying Lists with ListFragment Listing 9.2 Setting up the ArrayList of Crime objects (CrimeLab.java) public class CrimeLab { private ArrayList mCrimes; private static CrimeLab sCrimeLab; private Context mAppContext; private CrimeLab(Context appContext) { mAppContext = appContext; mCrimes = new ArrayList (); } public static CrimeLab get(Context c) { ... } public ArrayList getCrimes() { return mCrimes; } } public Crime getCrime(UUID id) { for (Crime c : mCrimes) { if (c.getId().equals(id)) return c; } return null; } Eventually, the ArrayList will contain user-created Crimes that can be saved and reloaded. But for now, populate the array list with 100 boring Crime objects (Listing 9.3). Listing 9.3 Generating crimes (CrimeLab.java) private CrimeLab(Context appContext) { mAppContext = appContext; mCrimes = new ArrayList (); for (int i = 0; i < 100; i++) { Crime c = new Crime(); c.setTitle("Crime #" + i); c.setSolved(i % 2 == 0); // Every other one mCrimes.add(c); } } Now you have a fully-loaded model layer and 100 crimes to get on screen. Creating a ListFragment Create a new class named CrimeListFragment. Click Browse to choose a superclass. Search for and select ListFragment – android.support.v4.app and then click Finish to generate CrimeListFragment. was introduced in Honeycomb, but it is duplicated in the support library. So compatibility is not a problem as long as you use the support library class, android.support.v4.app.ListFragment. ListFragment 170 An Abstract Activity for Hosting a Fragment In CrimeListFragment.java, override onCreate(Bundle) to set the title of the activity that will host this fragment. Listing 9.4 Adding onCreate(Bundle) to the new fragment (CrimeListFragment.java) public class CrimeListFragment extends ListFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.crimes_title); } } Notice the getActivity() method. This Fragment convenience method returns the hosting activity and allows a fragment to handle more of the activity’s affairs. Here you use it to call Activity.setTitle(int) to change what is displayed on the action bar (or title bar on older devices) to the value of the passed-in string resource. You are not going to override onCreateView(…) or inflate a layout for CrimeListFragment. The default implementation of a ListFragment inflates a layout that defines a fullscreen ListView, and you will use this layout for now. In later chapters, you will override CrimeListFragment.onCreateView(…) to add more advanced features. In strings.xml, add the string resource for the list activity title. Listing 9.5 Adding string resource for the new activity title (strings.xml) ... Solved? Crimes mCrimes; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.crimes_title); mCrimes = CrimeLab.get(getActivity()).getCrimes(); } } An Abstract Activity for Hosting a Fragment In a moment, you will create the CrimeListActivity class that is designed to host a CrimeListFragment. First, let’s set up a view for CrimeListActivity. 171 Chapter 9 Displaying Lists with ListFragment A generic fragment-hosting layout For CrimeListActivity, you can simply reuse the layout defined in activity_crime.xml (Listing 9.7). This layout provides a FrameLayout as a container view for a fragment, which is then named in the activity’s code. Listing 9.7 activity_crime.xml is already genericBecause activity_crime.xml does not name a particular fragment, you can use it for any activity hosting a single fragment. Let’s rename it activity_fragment.xml to reflect its larger scope. First, close activity_crime.xml in the editor if you have it open. Next, in the package explorer, right-click res/layout/activity_crime.xml. (Be sure to right-click activity_crime.xml and not fragment_crime.xml.) From the context menu, select Refactor → Rename... Rename this layout activity_fragment.xml. When you rename a resource, the references to it are updated automatically. On older versions of ADT, renaming a resource does not update references. If Eclipse is reporting an error in CrimeActivity.java, then you need to manually update the reference in CrimeActivity, as shown in Listing 9.8. Listing 9.8 Update layout file for CrimeActivity (CrimeActivity.java) public class CrimeActivity extends FragmentActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crime); setContentView(R.layout.activity_fragment); FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); } } if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } An abstract Activity class To create the CrimeListActivity class, you could reuse CrimeActivity’s code. Look back at the code you wrote for CrimeActivity (Listing 9.8). It is simple and almost generic. In fact, the only nongeneric code is the instantiation of the CrimeFragment before it is added to the FragmentManager. 172 An abstract Activity class Listing 9.9 CrimeActivity is almost generic (CrimeActivity.java) public class CrimeActivity extends FragmentActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment); FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); } } if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } Nearly every activity you will create in this book will require the same code. To avoid typing it again and again, you are going to stash it in an abstract class. Create a new class named SingleFragmentActivity in CriminalIntent’s package. Make this class a subclass of FragmentActivity and check the box marked abstract to make SingleFragmentActivity an abstract class (Figure 9.3). 173 Chapter 9 Displaying Lists with ListFragment Figure 9.3 Create abstract SingleFragmentActivity class Click Finish and add the following code to SingleFragmentActivity.java. Except for the highlighted portions, it is identical to your old CrimeActivity code. Listing 9.10 Add a generic superclass (SingleFragmentActivity.java) public abstract class SingleFragmentActivity extends FragmentActivity { protected abstract Fragment createFragment(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment); FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); } 174 } if (fragment == null) { fragment = createFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } An abstract Activity class In this code, you set the activity’s view to be inflated from activity_fragment.xml. Then you look for the fragment in the FragmentManager in that container, creating and adding it if it does not exist. The only difference between the code in Listing 9.10 and the code in CrimeActivity is an abstract method named createFragment() that you use to instantiate the fragment. Subclasses of SingleFragmentActivity will implement this method to return an instance of the fragment that the activity is hosting. Using an abstract class Try it out with CrimeListActivity. Create a new class named CrimeListActivity. You can set its superclass as SingleFragmentActivity in the wizard. Just browse to select the superclass, type in SingleFragmentActivity, and Eclipse will offer it as a choice. Select it and then click Finish. Figure 9.4 Selecting SingleFragmentActivity Eclipse will open CrimeListActivity.java, and it will already have a stub for createFragment(). Have this method return a new instance of CrimeListFragment. Listing 9.11 Implement CrimeListActivity (CrimeListActivity.java) public class CrimeListActivity extends SingleFragmentActivity { @Override protected Fragment createFragment() { return new CrimeListFragment(); } } It would be best if CrimeActivity worked the same way. Return to CrimeActivity.java. Remove the existing code from CrimeActivity and rewrite it as a subclass of SingleFragmentActivity, as shown in Listing 9.12. 175 Chapter 9 Displaying Lists with ListFragment Listing 9.12 Clean up CrimeActivity (CrimeActivity.java) public class CrimeActivity extends FragmentActivity SingleFragmentActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment); FragmentManager fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); } } if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } @Override protected Fragment createFragment() { return new CrimeFragment(); } will save you a lot of typing and time as you proceed through the book. And now your activity code is nice and tidy. SingleFragmentActivity Declaring CrimeListActivity Now that you have created CrimeListActivity, you must declare it in the manifest. In addition, you want the list of crimes to be the first screen that the user sees when CriminalIntent is launched, so CrimeListActivity should be the launcher activity. In the manifest, declare CrimeListActivity and move the launcher intent filter from CrimeActivity’s declaration to CrimeListActivity’s, as shown in Listing 9.13. 176 An abstract Activity class Listing 9.13 Declaring CrimeListActivity as the launcher activity (AndroidManifest.xml) ... CrimeListActivity is now the launcher activity. Run CriminalIntent, and you CrimeListActivity’s FrameLayout hosting an empty CrimeListFragment. will see Figure 9.5 Blank CrimeListActivity screen 177 Chapter 9 Displaying Lists with ListFragment shows a progress spinner in its implicit ListView when the ListView has nothing to display. You have given CrimeListFragment access to the array of Crimes, but you have not yet done anything to display their data in the ListView. You will start work on that next. ListFragment ListFragment, ListView, and ArrayAdapter Instead of a spinner, you want CrimeListFragment’s ListView to show list items to the user. Each list item should contain data about an instance of Crime. ListView is a subclass of ViewGroup, and each list item is displayed as a child View object of the ListView. Depending on the complexity of what you need the list to display, these child View objects can be complex View objects or very simple ones. Your first implementation of providing list items for display will be simple: a list item will only display the title of a Crime, and the View object will be a simple TextView. Figure 9.6 A ListView with child TextViews In Figure 9.6, you can see eleven TextViews and a hint of a twelfth. If you were able to scroll this screenshot, the ListView would show you additional TextViews – Crime�#12, Crime�#13, etc. So where do these View objects come from? Does the ListView have them ahead of time? That would be inefficient. A View object only needs to exist when it is on screen. Lists can be enormous, and unnecessarily creating and storing view objects for an entire list could cause performance and memory problems. 178 ListFragment, ListView, and ArrayAdapter The wiser course is to create view objects only as they are needed. The ListView asks for a view object when it needs to display a certain list item. Whom does the ListView ask? It asks its adapter. An adapter is a controller object that sits between the ListView and the data set containing the data that the ListView should display. The adapter is responsible for • creating the necessary view object • populating it with data from the model layer • returning the view object to the ListView An adapter is an instance of a class that implements the Adapter interface. You are going to use an instance of ArrayAdapter , which is an adapter that knows how to work with data in an array (or an ArrayList) of T type objects. Figure 9.7 shows the pedigree of the ArrayAdapter class. Each link in the chain provides another level of specialization. Figure 9.7 ArrayAdapter inherits from BaseAdapter, which inherits from ListAdapter... When the ListView needs a view object to display, it will have a conversation with its adapter. Figure 9.8 shows an example of a conversation that a ListView might initiate with its array adapter. 179 Chapter 9 Displaying Lists with ListFragment Figure 9.8 A scintillating ListView-Adapter conversation First, the ListView asks how many objects are in the array list by calling the adapter’s getCount() method. (This is important to avoid out of range errors.) Then the ListView calls the adapter’s getView(int, View, the position of the list item that the ListView is looking for. ViewGroup) method. The first argument is Within its implementation of getView(…), the adapter creates a view object from the correct item in the array list and returns that view object to the ListView. The ListView then adds the view object to itself as a child view, which gets the new view on screen. You will learn more about the mechanics of getView(…) later in the chapter when you override it to create custom list items. Creating an ArrayAdapter Initially, you are going to create a default implementation of ArrayAdapter for CrimeListFragment using the following constructor: public ArrayAdapter(Context context, int textViewResourceId, T[] objects) In the array adapter’s constructor, the first parameter is a Context object required to use the resource ID that is the second parameter. The resource ID identifies the layout that the ArrayAdapter will use to create the view object. The third parameter is the data set. In CrimeListFragment.java, create an instance of ArrayAdapter and make it the adapter for CrimeListFragment’s ListView (Listing 9.14). 180 Creating an ArrayAdapter Listing 9.14 Setting up the ArrayAdapter (CrimeListFragment.java) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.crimes_title); mCrimes = CrimeLab.get(getActivity()).getCrimes(); } ArrayAdapter adapter = new ArrayAdapter (getActivity(), android.R.layout.simple_list_item_1, mCrimes); setListAdapter(adapter); The setListAdapter(ListAdapter) method is a ListFragment convenience method that you can use to set the adapter of the implicit ListView managed by CrimeListFragment. The layout that you specify in the adapter’s constructor (android.R.layout.simple_list_item_1) is a pre-defined layout from the resources provided by the Android SDK. This layout has a TextView as its root element. Listing 9.15 Source code for android.R.layout.simple_list_item_1 You could specify a different layout in this constructor as long as its root element is a TextView. Thanks to the default behavior of ListFragment, you can run now, and the ListView will be instantiated and displayed and will start conversing with its adapter. Run CriminalIntent. List items will appear instead of a spinner. However, the text that each view is displaying is not particularly helpful to the user. 181 Chapter 9 Displaying Lists with ListFragment Figure 9.9 List items showing class names and memory addresses The default implementation of ArrayAdapter .getView(…) relies on toString(). It inflates the layout, finds the correct Crime object, and then calls toString() on the object to populate the TextView. does not currently override toString(), so it uses java.lang.Object’s implementation, which returns the fully-qualified class name and memory address of the object. Crime To have the adapter create a more useful view for a Crime, open Crime.java and override toString() to return the crime’s title. Listing 9.16 Overriding Crime.toString() (Crime.java) ... public Crime() { mId = UUID.randomUUID(); mDate = new Date(); } @Override public String toString() { return mTitle; } ... Run CriminalIntent again. Scroll through the list to see more crimes. 182 Responding to list item clicks Figure 9.10 Simple list items showing crime titles As you scroll, the ListView is calling getView(…) on the adapter to get the views that it needs to display. Responding to list item clicks To respond to the user touching a list item, you override another convenience method of ListFragment: public void onListItemClick(ListView l, View v, int position, long id) Whether the user “clicks” with a hardware button, a soft key, or a touch of a finger, the result still goes through onListItemClick(…). In CrimeListFragment.java, override onListItemClick(…) to have the adapter return the Crime for the item that was clicked and then log that Crime’s title. 183 Chapter 9 Displaying Lists with ListFragment Listing 9.17 Overriding onListItemClick(…) to log Crime title (CrimeListFragment.java) public class CrimeListFragment extends ListFragment { private static final String TAG = "CrimeListFragment"; ... @Override public void onListItemClick(ListView l, View v, int position, long id) { Crime c = (Crime)(getListAdapter()).getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); } The getListAdapter() method is a ListFragment convenience method that returns the adapter that is set on the ListFragment’s list view. You then call the adapter’s getItem(int) method using the position parameter of onListItemClick(…) and cast the result to a Crime. Run CriminalIntent again. Click a list item and check the log to confirm that the correct Crime was retrieved. Customizing List Items So far, each of your list items only displays the title of a Crime – the result of Crime.toString(). When you want to display more than just a string, you can create custom list items. Implementing custom list items requires two things: • creating a new layout in XML that defines the view for the list item • creating a subclass of ArrayAdapter that knows how to create, populate, and return the view defined in the new layout Creating the list item layout For CriminalIntent, a list item’s layout should include the crime’s title, its date, and whether the case has been solved (Figure 9.11). This layout calls for two TextViews and a CheckBox. Figure 9.11 A handful of custom list items 184 Creating the list item layout You create a new layout for a list item view the same way you do for the view of an activity or a fragment. In the package explorer, right-click the res/layout directory and choose New → Other... → Android�XML�File. In the dialog that appears, choose Layout as the resource type, name the file list_item_crime.xml, set the root element to RelativeLayout, and click Finish. In a RelativeLayout, you use certain layout parameters to arrange child views relative to the root layout and to each other. You are going to have the CheckBox align itself to the righthand side of the RelativeLayout. The two TextViews will align themselves relative to the CheckBox. Figure 9.12 shows the widgets for the custom list item layout. The CheckBox child must be defined first even though it will appear on the righthand side of the layout. This is because the TextViews will use the ID of the CheckBox as an attribute value. For the same reason, the title TextView must be defined before the date TextView. In a layout file, a widget must be defined before other widgets can use its ID in their definitions. 185 Chapter 9 Displaying Lists with ListFragment Figure 9.12 Custom list item layout (list_item_crime.xml) 186 Creating an adapter subclass Notice that when you use a widget’s ID in another widget’s definition, you do not include the +. The + sign is used to create the ID when it first appears in a layout file – typically in an android:id attribute. Also, notice that you are using literal strings instead of string resources for the android:text attributes. These text attributes are dummy text for developing and testing. The adapter will provide the values that the user will see. Because these particular strings will never be seen by users, there is little benefit to creating string resources. Your custom list item layout is complete, and you can turn to the next step – creating a custom adapter. Creating an adapter subclass Your custom layout is designed to display Crime-specific list items. The data for these list items must be obtained using Crime accessor methods, so you need a new adapter that knows how to work with Crime objects. In CrimeListFragment.java, create a subclass of ArrayAdapter as an inner class of CrimeListFragment. Listing 9.18 Adding custom adapter as inner class (CrimeListFragment.java) public void onListItemClick(ListView l, View v, int position, long id) { Crime c = (Crime)(getListAdapter()).getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); } private class CrimeAdapter extends ArrayAdapter { public CrimeAdapter(ArrayList crimes) { super(getActivity(), 0, crimes); } } } The call to the superclass constructor is required to properly hook up your dataset of Crimes. You will not be using a pre-defined layout, so you can pass 0 for the layout ID. The place to create and return a custom list item is the ArrayAdapter method: public View getView(int position, View convertView, ViewGroup parent) The convertView parameter is an existing list item that the adapter can reconfigure and return instead of creating a brand new object. Reusing view objects is a performance advantage because you do not constantly create and destroy the same type of object. A ListView can only display so many list items at a time, so there is no reason to have a number of unused view objects floating around and using up memory. In the CrimeAdapter class, override getView(…) to return a view inflated from your custom layout and populated with the correct Crime data (Listing 9.19). 187 Chapter 9 Displaying Lists with ListFragment Listing 9.19 Overriding getView(…) (CrimeListFragment.java) private class CrimeAdapter extends ArrayAdapter { public CrimeAdapter(ArrayList crimes) { super(getActivity(), 0, crimes); } @Override public View getView(int position, View convertView, ViewGroup parent) { // If we weren't given a view, inflate one if (convertView == null) { convertView = getActivity().getLayoutInflater() .inflate(R.layout.list_item_crime, null); } // Configure the view for this Crime Crime c = getItem(position); TextView titleTextView = (TextView)convertView.findViewById(R.id.crime_list_item_titleTextView); titleTextView.setText(c.getTitle()); TextView dateTextView = (TextView)convertView.findViewById(R.id.crime_list_item_dateTextView); dateTextView.setText(c.getDate().toString()); CheckBox solvedCheckBox = (CheckBox)convertView.findViewById(R.id.crime_list_item_solvedCheckBox); solvedCheckBox.setChecked(c.isSolved()); } } return convertView; In this implementation of getView(…), you first check to see if a recycled view was passed in. If not, you inflate one from the custom layout. Whether you are working with a new object or a recycled object, you call Adapter’s getItem(int) method to get the Crime for the current position in the list. After you have the correct Crime, you get a reference to each widget in the view object and configure it with the Crime’s data. Finally, you return the view object to the ListView. Now you can hook up your custom adapter in CrimeListFragment. At the top of CrimeListFragment.java, update the implementations of onCreate(…) and onListItemClick(…) to use a CrimeAdapter, as in Listing 9.20. Listing 9.20 Using a CrimeAdapter (CrimeListFragment.java) } ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, mCrimes); CrimeAdapter adapter = new CrimeAdapter(mCrimes); setListAdapter(adapter); public void onListItemClick(ListView l, View v, int position, long id) { Crime c = (Crime)(getListAdapter()).getItem(position); Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); } 188 Creating an adapter subclass Since you are casting to CrimeAdapter, you get the benefits of type-checking. CrimeAdapter can only hold Crime objects, so you no longer need to cast to Crime. Under ordinary circumstances, you would now be ready to run. However, because you have a CheckBox in your list item, there is one more change to make. A CheckBox is focusable by default. This means that a click on a list item will be interpreted as toggling the CheckBox and will not reach your onListItemClick(…) method. This internal quirk of ListView means that any focusable widget that appears in a list item layout (like a CheckBox or a Button) should be made non-focusable to ensure that a click on a list item will work as you expect. Because your CheckBox only reports information and is not tied to any application logic, there is a simple solution. You can update list_item_crime.xml to define the CheckBox as not focusable. Listing 9.21 Making the CheckBox non-focusable (list_item_crime.xml) ... ... Run your app and scroll through your custom list items. Click a list item and check the log to confirm that CrimeAdapter is returning the correct crimes. If you are able to get the application to run but the layout does not look right, return to list_item_crime.xml and recheck your layout. 189 Chapter 9 Displaying Lists with ListFragment Figure 9.13 Now with custom list items! 190 10 Using Fragment Arguments In this chapter, you will get the list and the detail parts of CriminalIntent working together. When a user clicks on an item in the list of crimes, a new CrimeActivity hosting a CrimeFragment will appear and display the details for a particular instance of Crime. Figure 10.1 Starting CrimeActivity from CrimeListActivity In GeoQuiz, you had one activity (QuizActivity) start another activity (CheatActivity). In CriminalIntent, you are going to start the CrimeActivity from a fragment. In particular, you will have CrimeListFragment start an instance of CrimeActivity. Starting an Activity from a Fragment Starting an activity from a fragment works nearly the same as starting an activity from another activity. You call the Fragment.startActivity(Intent) method, which calls the corresponding Activity method behind the scenes. In CrimeListFragment’s implementation of onListItemClick(…), replace logging the crime’s title with code that starts an instance of CrimeActivity. (Ignore the warning about the Crime variable being unused; you will be using it again in the next section.) Listing 10.1 Starting CrimeActivity (CrimeListFragment.java) public void onListItemClick(ListView l, View v, int position, long id) { // Get the Crime from the adapter Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); } // Start CrimeActivity Intent i = new Intent(getActivity(), CrimeActivity.class); startActivity(i); Here CrimeListFragment creates an explicit intent that names the CrimeActivity class. CrimeListFragment uses the getActivity() method to pass its hosting activity as the Context object that the Intent constructor requires. 191 Chapter 10 Using Fragment Arguments Run CriminalIntent. Click on any list item, and you will see a new CrimeActivity hosting a CrimeFragment. Figure 10.2 A blank CrimeFragment The CrimeFragment does not yet display the data for a specific Crime because you have not told it which Crime to display. Putting an extra You can tell CrimeFragment which Crime to display by making the mCrimeId an Intent extra. In onListItemClick(…), put the mCrimeId of the selected Crime on the intent that starts CrimeActivity. This code will have an error because you have not yet created the CrimeFragment.EXTRA_CRIME_ID key. Ignore the error; you will create the key next. Listing 10.2 Starting CrimeActivity with an extra (CrimeListFragment.java) public void onListItemClick(ListView l, View v, int position, long id) { // Get the Crime from the adapter Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); } 192 // Start CrimeActivity Intent i = new Intent(getActivity(), CrimeActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId()); startActivity(i); Retrieving an extra After creating an explicit intent, you call putExtra(…) and pass in a string key and the value to pair with it (the mCrimeId). In this case, you are calling putExtra(String, Serializable) because UUID is a Serializable object. Retrieving an extra The mCrimeId is now safely stashed in the intent that belongs to CrimeActivity. However, it is the CrimeFragment class that needs to retrieve and use that data. There are two ways a fragment can access data in its activity’s intent: an easy, direct shortcut and a complex, flexible implementation. First, you are going to try out the shortcut. Then you will implement the complex and flexible solution that involves fragment arguments. In the shortcut, CrimeFragment will simply use the getActivity() method to access the CrimeActivity’s intent directly. Return to CrimeFragment and add the key for the extra. Then, in onCreate(Bundle), retrieve the extra from CrimeActivity’s intent and use it to fetch the Crime. Listing 10.3 Retrieving the extra and fetching the Crime (CrimeFragment.java) public class CrimeFragment extends Fragment { public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id"; private Crime mCrime; ... public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCrime = new Crime(); UUID crimeId = (UUID)getActivity().getIntent() .getSerializableExtra(EXTRA_CRIME_ID); mCrime = CrimeLab.get(getActivity()).getCrime(crimeId); } In Listing 10.3, other than the call to getActivity(), the code is the same as if you were retrieving the extra from the activity’s code. The getIntent() method returns the Intent that was used to start CrimeActivity. You call getSerializableExtra(String) on the Intent to pull the UUID out into a variable. After you have retrieved the ID, you use it to fetch the Crime from CrimeLab. The CrimeLab.get(…) method requires a Context object, so CrimeFragment passes the CrimeActivity. Updating CrimeFragment’s view with Crime data Now that CrimeFragment fetches a Crime, its view can display that Crime’s data. Update onCreateView(…) to display the Crime’s title and solved status. (The code for displaying the date is already in place.) 193 Chapter 10 Using Fragment Arguments Listing 10.4 Updating view objects (CrimeFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { ... mTitleField = (EditText)v.findViewById(R.id.crime_title); mTitleField.setText(mCrime.getTitle()); mTitleField.addTextChangedListener(new TextWatcher() { ... }); ... mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved); mSolvedCheckBox.setChecked(mCrime.isSolved()); mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { ... }); ... } return v; Run CriminalIntent. Select Crime�#4 and watch a CrimeFragment instance with the correct crime data appear. Figure 10.3 The crime that you wanted to see 194 The downside to direct retrieval The downside to direct retrieval Having the fragment access the intent that belongs to the hosting activity makes for simple code. However, it costs you the encapsulation of your fragment. CrimeFragment is no longer a reusable building block because it expects that it will always be hosted by an activity whose Intent defines an extra named EXTRA_CRIME_ID. This may be a reasonable expectation on CrimeFragment’s part, but it means that CrimeFragment, as currently written, cannot be used with just any activity. A better solution is to stash the mCrimeId someplace that belongs to CrimeFragment rather than keeping it in CrimeActivity’s personal space. The CrimeFragment could then retrieve this data without relying on the presence of a particular extra in the activity’s intent. The “someplace” that belongs to a fragment is known as its arguments bundle. Fragment Arguments Every fragment instance can have a Bundle object attached to it. This bundle contains key-value pairs that work just like the intent extras of an Activity. Each pair is known as an argument. To create fragment arguments, you first create a Bundle object. Next, you use type-specific “put” methods of Bundle (similar to those of Intent) to add arguments to the bundle. Bundle args = new Bundle(); args.putSerializable(EXTRA_MY_OBJECT, myObject); args.putInt(EXTRA_MY_INT, myInt); args.putCharSequence(EXTRA_MY_STRING, myString); Attaching arguments to a fragment To attach the arguments bundle to a fragment, you call Fragment.setArguments(Bundle). Attaching arguments to a fragment must be done after the fragment is created but before it is added to an activity. To hit this window, Android programmers follow a convention of adding a static method named newInstance() to the Fragment class. This method creates the fragment instance and bundles up and sets its arguments. When the hosting activity needs an instance of that fragment, you have it call the newInstance() method rather than calling the constructor directly. The activity can pass in any required parameters to newInstance(…) that the fragment needs to create its arguments. In CrimeFragment, write a newInstance(UUID) method that accepts a UUID, creates an arguments bundle, creates a fragment instance, and then attaches the arguments to the fragment. Listing 10.5 Writing a newInstance(UUID) method (CrimeFragment.java) public static CrimeFragment newInstance(UUID crimeId) { Bundle args = new Bundle(); args.putSerializable(EXTRA_CRIME_ID, crimeId); CrimeFragment fragment = new CrimeFragment(); fragment.setArguments(args); } return fragment; 195 Chapter 10 Using Fragment Arguments Now CrimeActivity should call CrimeFragment.newInstance(UUID) when it needs to create a CrimeFragment. It will pass in the UUID it retrieved from its extra. Return to CrimeActivity and in createFragment(), retrieve the extra from CrimeActivity’s intent and pass it into CrimeFragment.newInstance(UUID). Listing 10.6 Using newInstance(UUID) (CrimeActivity.java) @Override protected Fragment createFragment() { return new CrimeFragment(); UUID crimeId = (UUID)getIntent() .getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID); } return CrimeFragment.newInstance(crimeId); Notice that the need for independence does not go both ways. CrimeActivity has to know plenty about CrimeFragment, like that it has a newInstance(UUID) method. This is fine. Hosting activities should know the specifics of how to host their fragments, but fragments should not have to know specifics about their activity. At least, not if you want to maintain the flexibility of independent fragments. Retrieving arguments When a fragment needs to access its arguments, it calls the Fragment method getArguments() and then one of the type-specific “get” methods of Bundle. Back in CrimeFragment.onCreate(…), replace your shortcut code with retrieving the UUID from the fragment arguments. Listing 10.7 Getting crime ID from the arguments (CrimeFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); UUID crimeId = (UUID)getActivity().getIntent() .getSerializableExtra(EXTRA_CRIME_ID); UUID crimeId = (UUID)getArguments().getSerializable(EXTRA_CRIME_ID); mCrime = CrimeLab.get(getActivity()).getCrime(crimeId); } Run CriminalIntent. The app will behave the same, but you should feel all warm and fuzzy inside for maintaining CrimeFragment’s independence. You are also well-prepared for the next chapter where you will implement more sophisticated navigation in CriminalIntent. Reloading the List There is one more detail to take care of. Run CriminalIntent, click a list item, and then modify that Crime’s details. These changes are saved to the model, but when you return to the list, the list view is unchanged. 196 Reloading the List The list view’s adapter needs to be informed that the data set has changed (or may have changed) so that it can refetch the data and reload the list. You can work with the ActivityManager’s back stack to reload the list at the right moment. When CrimeListFragment starts an instance of CrimeActivity, the CrimeActivity is put on top of the stack. This pauses and stops the instance of CrimeListActivity that was initially on top. When the user presses the Back button to return to the list, the CrimeActivity is popped off the stack and destroyed. At that point, the CrimeListActivity is started and resumed. Figure 10.4 CriminalIntent’s back stack When the CrimeListActivity is resumed, it receives a call to onResume() from the OS. When CrimeListActivity receives this call, its FragmentManager calls onResume() on the fragments that the activity is currently hosting. In this case, the only fragment is CrimeListFragment. In CrimeListFragment, override onResume() to reload the list. Listing 10.8 Reloading the list in onResume() (CrimeListFragment.java) @Override public void onResume() { super.onResume(); ((CrimeAdapter)getListAdapter()).notifyDataSetChanged(); } Why override onResume() to update the list view and not onStart() ? You cannot assume that your activity will be stopped when another activity is in front of it. If the other activity is transparent, your activity may just be paused. If your activity is paused and your update code is in onStart(), then the list will not be reloaded. In general, onResume() is the safest place to take action to update a fragment’s view. 197 Chapter 10 Using Fragment Arguments Run CriminalIntent. Select a crime and change its details. When you return to the list, you will immediately see your changes. You have made progress with CriminalIntent in the last two chapters. Let’s take a look at an updated object diagram. Figure 10.5 Updated object diagram for CriminalIntent Getting Results with Fragments In this chapter, you did not need a result back from the started activity. What if you did? Your code would look a lot like it did in GeoQuiz. Instead of using the Activity’s startActivityForResult(…) method, you would use Fragment.startActivityForResult(…). Instead of overriding Activity.onActivityResult(…), you would override Fragment.onActivityResult(…). public class CrimeListFragment extends ListFragment { private static final int REQUEST_CRIME = 1; ... public void onListItemClick(ListView l, View v, int position, long id) { // Get the Crime from the adapter Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); // Start CrimeActivity Intent i = new Intent(getActivity(), CrimeActivity.class); startActivityForResult(i, REQUEST_CRIME); } 198 Getting Results with Fragments } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CRIME) { // Handle result } } is similar to the Activity method with the same name. It includes some additional code to route the result to your fragment from its host activity. Fragment.startActivityForResult(Intent,int) Returning results from a fragment is a bit different. A fragment can receive a result from an activity, but it cannot have its own result. Only activities have results. So while Fragment has its own startActivityForResult(…) and onActivityResult(…) methods, it does not have any setResult(…) methods. Instead, you tell the host activity to return a value. Like this: public class CrimeFragment extends Fragment { ... } public void returnResult() { getActivity().setResult(Activity.RESULT_OK, null); } You will get a chance to return an activity result from a fragment in CriminalIntent in Chapter 20. 199 This page intentionally left blank 11 Using ViewPager In this chapter, you will create a new activity to host CrimeFragment. This activity’s layout will consist of an instance of ViewPager. Adding a ViewPager to your UI lets users navigate between list items by swiping across the screen to “page” forward or backward through the crimes. Figure 11.1 Swiping to page through crimes Figure 11.2 shows an updated diagram for CriminalIntent. The new activity will be named CrimePagerActivity and will take the place of CrimeActivity. Its layout will consist of a ViewPager. 201 Chapter 11 Using ViewPager Figure 11.2 Layout diagram for CrimePagerActivity The only new objects you need to create are within the dashed rectangle in the diagram above. Nothing else in CriminalIntent needs to change to implement paging between detail views. In particular, you will not have to touch the CrimeFragment class thanks to the work you did in Chapter 10 to ensure CrimeFragment’s independence. Here are the tasks ahead in this chapter: • create the CrimePagerActivity class • define a view hierarchy that consists of a ViewPager • wire up the ViewPager and its adapter in CrimePagerActivity • modify CrimeListFragment.onListItemClick(…) to start CrimePagerActivity instead of CrimeActivity Creating CrimePagerActivity CrimePagerActivity ViewPager. will be a subclass of FragmentActivity. It will create and manage the Create a new class named CrimePagerActivity. Make its superclass FragmentActivity. In onCreate(Bundle), simply call through to the superclass – you will create your instance of ViewPager in a moment. 202 Laying out views in code Listing 11.1 Set up ViewPager (CrimePagerActivity.java) public class CrimePagerActivity extends FragmentActivity { private ViewPager mViewPager; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } Laying out views in code Elsewhere in this book, you have defined view layouts in layout XML files. This is generally a good idea, but there is nothing in Android that requires you to define your layout this way. Your view hierarchy in this chapter will be simple – just a single view. So instead of using an XML file, let’s define your hierarchy in code. Since you only have one view here, this is not an involved task. No magic is necessary to create a view: just call its constructor. Unfortunately, that does not leave you completely free of XML. Resource IDs are still required for some components. ViewPager is one of them. The FragmentManager requires that any view used as a fragment container must have an ID. ViewPager is a fragment container, so you will need to assign an ID to it. That gives you the following list of things to do: • create a resource ID for your ViewPager • create an instance of ViewPager and assign it to mViewPager. • configure it by assigning a resource ID to it. • set your ViewPager as the content view of your activity. Standalone ID resources Defining a standalone resource ID is not unlike defining a string resource: you create an item element in an XML file in res/values. Create a new Android XML resource file called res/values/ids.xml to store your IDs in, and add an ID called viewPager: Listing 11.2 Create standalone resource ID (res/values/ids.xml) With your ID created, you are ready to create and show your ViewPager. Instantiate your ViewPager and set it as the content view: 203 Chapter 11 Using ViewPager Listing 11.3 Create content view programmatically (CrimePagerActivity.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewPager); setContentView(mViewPager); } The ViewPager class is from the support library. Unlike Fragment, ViewPager is only available in the support library; there is not a “standard” ViewPager class in a later SDK. ViewPager and PagerAdapter A ViewPager is like an AdapterView (the superclass of ListView) in some ways. An AdapterView requires an Adapter to provide views. A ViewPager requires a PagerAdapter. However, the conversation between ViewPager and PagerAdapter is much more involved than the conversation between AdapterView and Adapter. Luckily, you can use FragmentStatePagerAdapter, a subclass of PagerAdapter, to take care of many of the details. FragmentStatePagerAdapter will boil down the conversation to two simple methods: getCount() and getItem(int). When your getItem(int) method is called for a position in your array of crimes, you will return a CrimeFragment configured to display the crime at that position. In CrimePagerActivity, add the following code to set the ViewPager’s pager adapter and implement its getCount() and getItem(int) methods. 204 Integrating CrimePagerActivity Listing 11.4 Set up pager adapter (CrimePagerActivity.java) public class CrimePagerActivity extends FragmentActivity { private ViewPager mViewPager; private ArrayList mCrimes; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewPager); setContentView(mViewPager); mCrimes = CrimeLab.get(this).getCrimes(); FragmentManager fm = getSupportFragmentManager(); mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) { @Override public int getCount() { return mCrimes.size(); } } } }); @Override public Fragment getItem(int pos) { Crime crime = mCrimes.get(pos); return CrimeFragment.newInstance(crime.getId()); } Let’s go through this code. In the first line, you get your data set from CrimeLab – the ArrayList of crimes. Next, you get the activity’s instance of FragmentManager. Then you set the adapter to be an unnamed instance of FragmentStatePagerAdapter. Creating the FragmentStatePagerAdapter requires the FragmentManager. Remember that FragmentStatePagerAdapter is your agent managing the conversation with ViewPager. For your agent to do its job with the fragments you return in getItem(int), it needs to be able to add them to your activity. That is why it needs your FragmentManager. (What exactly is your agent doing? The short story is that it is adding the fragments you return to your activity and helping ViewPager identify the fragments’ views so that they can be placed correctly. More details are in the For the More Curious section at the end of the chapter.) The pager adapter’s two methods are straightforward. The getCount() method returns how many items there are in the array list. The getItem(int) method is where the magic happens. It fetches the Crime instance for the given position in the dataset. It then uses that Crime’s ID to create and return a properly configured CrimeFragment. Integrating CrimePagerActivity Now you can begin the process of decommissioning CrimeActivity and putting CrimePagerActivity in its place. First, you want pressing a list item in CrimeListFragment to start an instance of CrimePagerActivity instead of CrimeActivity. 205 Chapter 11 Using ViewPager Return to CrimeListFragment.java and modify onListItemClick(…) to start a CrimePagerActivity. Listing 11.5 Fire it up (CrimeListFragment.java) @Override public void onListItemClick(ListView l, View v, int position, long id) { // Get the Crime from the adapter Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); // Start CrimeActivity Intent i = new Intent(getActivity(), CrimeActivity.class); // Start CrimePagerActivity with this crime Intent i = new Intent(getActivity(), CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId()); startActivity(i); } You also need to add CrimePagerActivity to the manifest so that the OS can start it. While you are in the manifest, remove CrimeActivity’s declaration. Listing 11.6 Add CrimePagerActivity to manifest (AndroidManifest.xml) ... Finally, to keep your project tidy, delete CrimeActivity.java from the package explorer. Run CriminalIntent. Press Crime�#0 to view its details. Then swipe left and right to browse more crimes. Notice that the paging is smooth and there is no delay in loading. By default, ViewPager loads the item currently on screen plus one neighboring page in each direction so that the response to a swipe is immediate. You can tweak how many neighboring pages are loaded by calling setOffscreenPageLimit(int). But all is not yet perfect with your ViewPager. Press Back to return to the list of crimes and press a different item. You will see the first crime displayed again instead of the crime that you asked for. By default, the ViewPager shows the first item in its PagerAdapter. You can have it show the crime that was selected by setting the ViewPager’s current item to the index of the selected crime. At the end of CrimePagerActivity.onCreate(…), find the index of the crime to display by looping through and checking each crime’s ID. When you find the Crime instance whose mId matches the crimeId in the intent extra, set the current item to the index of that Crime. 206 Integrating CrimePagerActivity Listing 11.7 Set initial pager item (CrimePagerActivity.java) public class CrimePagerActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { ... FragmentManager fm = getSupportFragmentManager(); mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) { ... }); } } UUID crimeId = (UUID)getIntent() .getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID); for (int i = 0; i < mCrimes.size(); i++) { if (mCrimes.get(i).getId().equals(crimeId)) { mViewPager.setCurrentItem(i); break; } } Run CriminalIntent. Selecting any list item should display the details of the correct Crime. There is one more feature you can add. You can replace the activity’s title that appears on the action bar (or the title bar on older devices) with the title of the current Crime. To make this happen, implement the ViewPager.OnPageChangeListener interface. Listing 11.8 Add OnPageListener (CrimePagerActivity.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewPager); setContentView(mViewPager); mCrimes = CrimeLab.get(this).getCrimes(); FragmentManager fm = getSupportFragmentManager(); mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) { ... }); mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { public void onPageScrollStateChanged(int state) { } public void onPageScrolled(int pos, float posOffset, int posOffsetPixels) { } }); } public void onPageSelected(int pos) { Crime crime = mCrimes.get(pos); if (crime.getTitle() != null) { setTitle(crime.getTitle()); } } ... 207 Chapter 11 Using ViewPager OnPageChangeListener is how you listen for changes in the page currently being displayed by ViewPager. When the page changes, you set CrimePagerActivity’s title to the title of the Crime. You are only interested in which page is currently selected, so you only implement onPageSelected(…). The onPageScrolled(…) method tells you exactly where your page is going to be, and onPageScrollStateChanged(…) tells you whether the page animation is being actively dragged, settling to a steady state, or idling. Run CriminalIntent to see that each swipe updates the activity’s title to the mTitle of the current Crime. And that is it. Your ViewPager is now fully-armed and operational. FragmentStatePagerAdapter vs. FragmentPagerAdapter There is another PagerAdapter type that you can use called FragmentPagerAdapter. FragmentPagerAdapter is used exactly like FragmentStatePagerAdapter. It only differs in how it unloads your fragments when they are no longer needed. Figure 11.3 FragmentStatePagerAdapter’s fragment management With FragmentStatePagerAdapter, your unneeded fragment is destroyed. A transaction is committed to completely remove the fragment from your activity’s FragmentManager. The “state” in FragmentStatePagerAdapter comes from the fact that it will save out your fragment’s Bundle from onSaveInstanceState(Bundle) when it is destroyed. When the user navigates back, the new fragment will be restored using that instance state. By comparison, FragmentPagerAdapter does nothing of the kind. When your fragment is no longer needed, FragmentPagerAdapter calls detach(Fragment) on the transaction instead of remove(Fragment). This destroys the fragment’s view, but leaves the fragment instance alive in the FragmentManager. So the fragments created by FragmentPagerAdapter are never destroyed. 208 For the More Curious: How ViewPager Really Works Figure 11.4 FragmentPagerAdapter’s fragment management Which kind of adapter you want to use depends on your application. In general, FragmentStatePagerAdapter is more frugal with memory. CriminalIntent is displaying what could be a long list of crimes, each of which will eventually include a photo. You do not want to keep all that information in memory, so you use FragmentStatePagerAdapter. On the other hand, if your interface has a small, fixed number of fragments, FragmentPagerAdapter is safe and appropriate. The most common example of this scenario is a tabbed interface. Some detail views have enough details to require two screens, so the details are split across multiple tabs. Adding a swipable ViewPager to this interface makes the app tactile. Keeping these fragments in memory can make your controller code easier to manage, and because this style of interface usually has only two or three fragments per activity, there is little danger of running low on memory. For the More Curious: How ViewPager Really Works The ViewPager and PagerAdapter classes handle many things for you behind the scenes. This section will supply more details about what is going on back there. Two caveats before we get into this discussion: First, according to the documentation, ViewPager is still under development, so these interfaces could change in the future. Second, you do not need to understand the nitty-gritty details in most cases. But if you need to implement the PagerAdapter interface yourself, you will need to know how the ViewPager-PagerAdapter relationship differs from an ordinary AdapterView-Adapter relationship. So why is ViewPager not an AdapterView? There is an AdapterView subclass called Gallery that behaves similarly. Why not use that? Using an AdapterView in this case would be a lot of work because you could not use your existing Fragment. An Adapter expects you to provide a View instantly. However, your FragmentManager 209 Chapter 11 Using ViewPager determines when your fragment’s view is created, not you. So when Gallery comes knocking at your Adapter’s door for your fragment’s view, you will not be able to create the fragment and provide its view immediately. This is the reason ViewPager exists. Instead of an Adapter, it uses a class called PagerAdapter. PagerAdapter is more complicated than Adapter because it does more of the work of managing views than Adapter does. Here are the basics: Instead of a getView(…) method that returns a view, PagerAdapter has the following methods: public Object instantiateItem(ViewGroup container, int position) public void destroyItem(ViewGroup container, int position, Object object) public abstract boolean isViewFromObject(View view, Object object) PagerAdapter.instantiateItem(ViewGroup, int) tells the pager adapter to create an item view for a given position and add it to a container ViewGroup, and destroyItem(ViewGroup, int, Object) tells it to destroy that item. Note that instantiateItem(ViewGroup, int) does not say to create the view right now. The PagerAdapter could create the view at any time after that. Once the view has been created, ViewPager will notice it at some point. To figure out which item’s view it is, it calls isViewFromObject(View, Object). The Object parameter is an object received from a call to instantiateItem(ViewGroup, int). So if ViewPager calls instantiateItem(ViewGroup, 5) and receives object A, isViewFromObject(View, A) should return true if the View passed in is for item 5, and false otherwise. This is a complicated process for the ViewPager, but it is less complicated for the PagerAdapter, which only needs to be able to create views, destroy views, and identify which object a view comes from. This loose requirement gives a PagerAdapter implementation enough wiggle room to create and add a new fragment inside instantiateItem(ViewGroup, int) and return the fragment as the Object to keep track of. Then isViewFromObject(View, Object) looks like this: @Override public boolean isViewFromObject(View view, Object object) { return ((Fragment)object).getView() == view; } Implementing all those PagerAdapter overrides would be a pain to do every time you needed to use ViewPager. Thank goodness for FragmentPagerAdapter and FragmentStatePagerAdapter. 210 12 Dialogs Dialogs demand attention and input from the user. They are useful for presenting a choice or important information. In this chapter, you will add a dialog in which users can change the date of a crime. Pressing the date button in CrimeFragment will present this dialog (Figure 12.1). Figure 12.1 A dialog for picking the date of a crime The dialog in Figure 12.1 is an instance of AlertDialog, a subclass of Dialog. AlertDialog is the allpurpose Dialog subclass that you will use most often. (There is a DatePickerDialog subclass of AlertDialog that sounds perfect for what you are doing. However, as of this writing DatePickerDialog has some buggy behavior, and using AlertDialog is easier than working around these bugs.) The AlertDialog in Figure 12.1 is wrapped in an instance of DialogFragment, a subclass of Fragment. It is possible to display an AlertDialog without a DialogFragment, but Android does not recommend 211 Chapter 12 Dialogs it. Having the dialog managed by the FragmentManager gives you more options for presenting the dialog. In addition, a bare AlertDialog will vanish if the device is rotated. On the other hand, if the AlertDialog is wrapped in a fragment, then the dialog will be recreated and put on screen after rotation. For CriminalIntent, you are going to create a DialogFragment subclass named DatePickerFragment. Within DatePickerFragment, you will create and configure an instance of AlertDialog that displays a DatePicker widget. DatePickerFragment will be hosted by CrimePagerActivity. Figure 12.2 shows you an overview of these relationships. Figure 12.2 Object diagram for two fragments hosted by CrimePagerActivity Your first tasks are • creating the DatePickerFragment class • building an AlertDialog • getting the dialog on screen via the FragmentManager Later in the chapter, you will wire up the DatePicker and pass the necessary data between CrimeFragment and DatePickerFragment. Before you get started, add the string resource shown in Listing 12.1. 212 Creating a DialogFragment Listing 12.1 Add string for dialog title (values/strings.xml)... ... Creating a DialogFragment Create a new class named DatePickerFragment and make its superclass the support library’s version of DialogFragment: android.support.v4.app.DialogFragment. DialogFragment includes the following method: public Dialog onCreateDialog(Bundle savedInstanceState) The FragmentManager of the hosting activity calls this method as part of putting the DialogFragment on screen. In DatePickerFragment.java, add an implementation of onCreateDialog(…) that builds an AlertDialog with a title and one OK button. (You will add the DatePicker widget later.) Listing 12.2 Creating a DialogFragment (DatePickerFragment.java) public class DatePickerFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(getActivity()) .setTitle(R.string.date_picker_title) .setPositiveButton(android.R.string.ok, null) .create(); } } In this implementation, you use the AlertDialog.Builder class that provides a fluent interface for constructing an AlertDialog instance. First, you pass a Context into the AlertDialog.Builder constructor, which returns an instance of AlertDialog.Builder. Next, you call two AlertDialog.Builder methods to configure your dialog: public AlertDialog.Builder setTitle(int titleId) public AlertDialog.Builder setPositiveButton(int textId, DialogInterface.OnClickListener listener) This setPositiveButton(…) method accepts a string resource and an object that implements DialogInterface.OnClickListener. In Listing 12.2, you pass in an Android constant for OK and null for the listener parameter. You will implement a listener later in the chapter. (A positive button is what the user should press to accept what the dialog presents. There are two other buttons that you can add to an AlertDialog: a negative button and a neutral button. These designations determine the positions of the buttons in the dialog (when there is more than one). On Froyo and 213 Chapter 12 Dialogs Gingerbread devices, the positive button is left-most. On newer devices, the order is flipped, and the positive button is right-most.) Finally, you finish building the dialog with a call to AlertDialog.Builder.create(), which returns the configured AlertDialog instance. There is more that you can do with AlertDialog and AlertDialog.Builder, and the details are wellcovered in the developer documentation. For now, let’s move on to the mechanics of getting your dialog on screen. Showing a DialogFragment Like all fragments, instances of DialogFragment are managed by the FragmentManager of the hosting activity. To get a DialogFragment added to the FragmentManager and put on screen, you can call the following methods on the fragment instance: public void show(FragmentManager manager, String tag) public void show(FragmentTransaction transaction, String tag) The string parameter uniquely identifies the DialogFragment in the FragmentManager’s list. Whether you use the FragmentManager or FragmentTransaction version is up to you – if you pass in a FragmentManager, a transaction will automatically be created and committed for you. Here, you will pass in a FragmentManager. In CrimeFragment, add a constant for the DatePickerFragment’s tag. Then, in onCreateView(…), remove the code that disables the date button and set a View.OnClickListener that shows a DatePickerFragment when the date button is pressed. Listing 12.3 Show your DialogFragment (CrimeFragment.java) public class CrimeFragment extends Fragment { public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id"; private static final String DIALOG_DATE = "date"; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { ... mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setText(mCrime.getDate().toString()); mDateButton.setEnabled(false); mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { FragmentManager fm = getActivity() .getSupportFragmentManager(); DatePickerFragment dialog = new DatePickerFragment(); dialog.show(fm, DIALOG_DATE); } }); 214 Setting a dialog’s contents mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved); ... } } return v; ... Run CriminalIntent and press the date button to see the dialog. Press the OK button to dismiss the dialog. Figure 12.3 An AlertDialog with a title and a button Setting a dialog’s contents Next, you are going to add a DatePicker widget to your AlertDialog using the following AlertDialog.Builder method: public AlertDialog.Builder setView(View view) This method configures the dialog to display the passed-in View object between the dialog’s title and its button(s). In the package explorer, create a new layout file named dialog_date.xml and make its root element DatePicker. This layout will consist of a single View object – a DatePicker – that you will inflate and pass into setView(…). Configure the DatePicker as shown in Figure 12.4. 215 Chapter 12 Dialogs Figure 12.4 DatePicker layout (layout/dialog_date.xml) In DatePickerFragment.onCreateDialog(…), inflate this view and then set it on the dialog. Listing 12.4 Add DatePicker to AlertDialog (DatePickerFragment.java) @Override public Dialog onCreateDialog(Bundle savedInstanceState) { View v = getActivity().getLayoutInflater() .inflate(R.layout.dialog_date, null); } return new AlertDialog.Builder(getActivity()) .setView(v) .setTitle(R.string.date_picker_title) .setPositiveButton(android.R.string.ok, null) .create(); Run CriminalIntent. Press the date button to confirm that the dialog now presents a DatePicker. Figure 12.5 An AlertDialog with a DatePicker 216 Passing Data Between Two Fragments You may be wondering why you went to the trouble of defining and inflating a layout when you could have created the DatePicker object in code like this: @Override public Dialog onCreateDialog(Bundle savedInstanceState) { DatePicker dp = new DatePicker(getActivity()); } return new AlertDialog.Builder(getActivity()) .setView(dp) ... .create(); Using a layout makes modifications easy if you change your mind about what the dialog should present. For instance, what if you wanted a TimePicker next to the DatePicker in this dialog? If you are already inflating a layout, you can simply update the layout file, and the new view will appear. Your dialog is on screen and looks good. In the next section, you will wire it up to present the Crime’s date and allow the user to change it. Passing Data Between Two Fragments You have passed data between two activities, and you have passed data between two fragment-based activities. Now you need to pass data between two fragments that are hosted by the same activity – CrimeFragment and DatePickerFragment (Figure 12.6). Figure 12.6 Conversation between CrimeFragment and DatePickerFragment To get the crime’s date to DatePickerFragment, you are going to write a newInstance(Date) method and make the Date an argument on the fragment. To get the new date back to the CrimeFragment so that it can update the model layer and its own view, you will package up the date as an extra on an Intent and pass this Intent in a call to CrimeFragment.onActivityResult(…). 217 Chapter 12 Dialogs Figure 12.7 Sequence of events between CrimeFragment and DatePickerFragment It may seem strange to call Fragment.onActivityResult(…) given that the hosting activity receives no call to Activity.onActivityResult(…) in this interaction. However, using onActivityResult(…) to pass data back from one fragment to another not only works, but it also offers some flexibility in how you present a dialog fragment, as you will see later in the chapter. Passing data to DatePickerFragment To get data into your DatePickerFragment, you are going to stash the date in DatePickerFragment’s arguments bundle, where the DatePickerFragment can access it. Creating and setting fragment arguments is typically done in a newInstance() method that replaces the fragment constructor. In DatePickerFragment.java, add a newInstance(Date) method. Listing 12.5 Adding a newInstance(Date) method (DatePickerFragment.java) public class DatePickerFragment extends DialogFragment { public static final String EXTRA_DATE = "com.bignerdranch.android.criminalintent.date"; private Date mDate; public static DatePickerFragment newInstance(Date date) { Bundle args = new Bundle(); args.putSerializable(EXTRA_DATE, date); DatePickerFragment fragment = new DatePickerFragment(); fragment.setArguments(args); } } 218 ... return fragment; Passing data to DatePickerFragment In CrimeFragment, remove the call to the DatePickerFragment constructor and replace it with a call to DatePickerFragment.newInstance(Date). Listing 12.6 Add call to newInstance() (CrimeFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { ... mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { FragmentManager fm = getActivity() .getSupportFragmentManager(); DatePickerFragment dialog = new DatePickerFragment(); DatePickerFragment dialog = DatePickerFragment .newInstance(mCrime.getDate()); dialog.show(fm, DIALOG_DATE); } }); } return v; needs to initialize the DatePicker using the information held in the Date. However, initializing the DatePicker requires integers for the month, day, and year. Date is more of a timestamp and cannot provide integers like this directly. DatePickerFragment To get the integers you need, you must create a Calendar object and use the Date to configure the Calendar. Then you can retrieve the required information from the Calendar. In onCreateDialog(…), get the Date from the arguments and use it and a Calendar to initialize the DatePicker. 219 Chapter 12 Dialogs Listing 12.7 Date extraction and DatePicker initialization (DatePickerFragment.java) @Override public Dialog onCreateDialog(Bundle savedInstanceState) { mDate = (Date)getArguments().getSerializable(EXTRA_DATE); // Create a Calendar to get the year, month, and day Calendar calendar = Calendar.getInstance(); calendar.setTime(mDate); int year = calendar.get(Calendar.YEAR); int month = calendar.get(Calendar.MONTH); int day = calendar.get(Calendar.DAY_OF_MONTH); View v = getActivity().getLayoutInflater() .inflate(R.layout.dialog_date, null); DatePicker datePicker = (DatePicker)v.findViewById(R.id.dialog_date_datePicker); datePicker.init(year, month, day, new OnDateChangedListener() { public void onDateChanged(DatePicker view, int year, int month, int day) { // Translate year, month, day into a Date object using a calendar mDate = new GregorianCalendar(year, month, day).getTime(); }); } } // Update argument to preserve selected value on rotation getArguments().putSerializable(EXTRA_DATE, mDate); ... When you initialize the DatePicker, you also set an OnDateChangedListener on it. When the user changes the date in the DatePicker, you update the Date to reflect the change. In the next section, you will pass this Date back to CrimeFragment. At the end of onDateChanged(…), you write the Date back to the fragment’s arguments. You do this to preserve the value of mDate in case of rotation. If the device is rotated while the DatePickerFragment is on screen, then the FragmentManager will destroy the current instance and create a new one. When the new instance is created, the FragmentManager will call onCreateDialog(…) on it, and the instance will get the saved date from the arguments. It is a simpler way to preserve the value than saving out its state in onSaveInstanceState(…). (If you have worked more with fragments, you may be wondering why not “retain” DatePickerFragment instead? Using a retained fragment is a great tool for handling rotation that you will learn about in Chapter 14. Unfortunately, DialogFragment currently has a bug that causes retained instances to misbehave, so retaining DatePickerFragment is not a good option.) Now CrimeFragment is successfully telling DatePickerFragment what date to show. You can run CriminalIntent and make sure that everything works as before. Returning data to CrimeFragment To have CrimeFragment receive the date back from DatePickerFragment, you need a way to keep track of the relationship between the two fragments. 220 Returning data to CrimeFragment With activities, you call startActivityForResult(…), and the ActivityManager keeps track of the parent-child activity relationship. When the child activity dies, the ActivityManager knows which activity should receive the result. Setting a target fragment You can create a similar connection by making CrimeFragment the target fragment of DatePickerFragment. To create this relationship, you call the following Fragment method: public void setTargetFragment(Fragment fragment, int requestCode) This method accepts the fragment that will be the target and a request code just like the one you send in startActivityForResult(…). The target fragment can use the request code later to tell which fragment is reporting back. The FragmentManager keeps track of the target fragment and request code. You can retrieve them by calling getTargetFragment() and getTargetRequestCode() on the fragment that has set the target. In CrimeFragment.java, create a constant for the request code and then make CrimeFragment the target fragment of the DatePickerFragment instance. Listing 12.8 Set target fragment (CrimeFragment.java) public class CrimeFragment extends Fragment { public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id"; private static final String DIALOG_DATE = "date"; private static final int REQUEST_DATE = 0; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { ... mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { FragmentManager fm = getActivity() .getSupportFragmentManager(); DatePickerFragment dialog = DatePickerFragment .newInstance(mCrime.getDate()); dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE); dialog.show(fm, DIALOG_DATE); } }); } } return v; ... Sending data to the target fragment Now that you have a connection between CrimeFragment and DatePickerFragment, you need to send the date back to CrimeFragment. You are going to put the date on an Intent as an extra. 221 Chapter 12 Dialogs What method will you use to send this intent to the target fragment? Oddly enough, you will have DatePickerFragment pass it into CrimeFragment.onActivityResult(int, int, Intent). is the method that the ActivityManager calls on the parent activity after the child activity dies. When dealing with activities, you do not call Activity.onActivityResult(…) yourself; that is the ActivityManager’s job. After the activity has received the call, the activity’s FragmentManager then calls Fragment.onActivityResult(…) on the appropriate fragment. Activity.onActivityResult(…) When dealing with two fragments hosted by the same activity, you can borrow Fragment.onActivityResult(…) and call it directly on the target fragment to pass back data. It has exactly what you need: • a request code that matches the code passed into setTargetFragment(…) to tell the target who is returning the result • a result code to determine what action to take • an Intent that can have extra data In DatePickerFragment, create a private method that creates an intent, puts the date on it as an extra, and then calls CrimeFragment.onActivityResult(…). In onCreateDialog(…), replace the null parameter of setPositiveButton(…) with an implementation of DialogInterface.OnClickListener that calls the private method and passes in a result code. Listing 12.9 Calling back to your target (DatePickerFragment.java) private void sendResult(int resultCode) { if (getTargetFragment() == null) return; Intent i = new Intent(); i.putExtra(EXTRA_DATE, mDate); } getTargetFragment() .onActivityResult(getTargetRequestCode(), resultCode, i); @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... } 222 return new AlertDialog.Builder(getActivity()) .setView(v) .setTitle(R.string.date_picker_title) .setPositiveButton(android.R.string.ok, null) .setPositiveButton( android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { sendResult(Activity.RESULT_OK); } }) .create(); Returning data to CrimeFragment In CrimeFragment, override onActivityResult(…) to retrieve the extra, set the date on the Crime, and refresh the text of the date button. Listing 12.10 Responding to the dialog (CrimeFragment.java) @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) return; if (requestCode == REQUEST_DATE) { Date date = (Date)data .getSerializableExtra(DatePickerFragment.EXTRA_DATE); mCrime.setDate(date); mDateButton.setText(mCrime.getDate().toString()); } } The code that sets the button’s text is identical to code you call in onCreateView(…). To avoid setting the text in two places, encapsulate this code in a private updateDate() method and then call it in onCreateView(…) and onActivityResult(…). Listing 12.11 Cleaning up with updateDate() (CrimeFragment.java) public class CrimeFragment extends Fragment { ... private void updateDate() { mDateButton.setText(mCrime.getDate().toString()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_crime, parent, false); ... mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setText(mCrime.getDate().toString()); updateDate(); ... } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) return; if (requestCode == REQUEST_DATE) { Date date = (Date)data .getSerializableExtra(DatePickerFragment.EXTRA_DATE); mCrime.setDate(date); mDateButton.setText(mCrime.getDate().toString()); updateDate(); } } Now the circle is complete. The dates must flow. He who controls the dates controls time itself. Run CriminalIntent to ensure that you can, in fact, control the dates. Change the date of a Crime and 223 Chapter 12 Dialogs confirm that the new date appears in CrimeFragment’s view. Then return to the list of crimes and check the Crime’s date to ensure that the model layer was updated. More flexibility in presenting a DialogFragment Using onActivityResult(…) to send data back to a target fragment is especially nice when you are writing an app that needs lots of input from the user and more room to ask for it. And you want this app working well on phones and tablets. On a phone, you do not have much screen real estate, so you would likely use an activity with a fullscreen fragment to ask the user for input. This child activity would be started by a fragment of the parent activity calling startActivityForResult(…). On the death of the child activity, the parent activity would receive a call to onActivityResult(…), which would be forwarded to the fragment that started the child activity. Figure 12.8 Inter-activity communication on phones On a tablet, where you have plenty of room, it is often better to present a DialogFragment to the user to get the same input. In this case, you set the target fragment and call show(…) on the dialog fragment. When dismissed, the dialog fragment calls onActivityResult(…) on its target. 224 Challenge: More Dialogs Figure 12.9 Inter-fragment communication on tablets The fragment’s onActivityResult(…) will always be called, whether the fragment started an activity or showed a dialog. So you can use the same code for different presentations. When setting things up to use the same code for a full-screen fragment or a dialog fragment, you can override DialogFragment.onCreateView(…) instead of onCreateDialog(…) to prepare for both presentations. Challenge: More Dialogs For an easy challenge, write another dialog fragment named TimePickerFragment that allows the user to select what time of day the crime occurred using a TimePicker widget. Add another button to CrimeFragment that will display a TimePickerFragment. For a harder challenge, stay with a single button interface, but have that button display a dialog that offers the user a choice: change the time or change the date. Then fire up a second dialog when the user makes a selection. 225 This page intentionally left blank 13 Audio Playback Using MediaPlayer For the next three chapters, you are going to put CriminalIntent on hiatus and build another application. This app will play a historical audio file using the MediaPlayer class. Figure 13.1 Hello, Moon! is the Android class for audio and video playback. It can play media from different sources (like a local file or streamed from the Internet) and in many different formats (like WAV, MP3, Ogg Vorbis, MPEG-4, and 3GPP). MediaPlayer Create a new project named HelloMoon. In the first dialog, select Holo�Dark for the app’s theme. 227 Chapter 13 Audio Playback Using MediaPlayer Figure 13.2 Creating HelloMoon with Holo Dark theme Click Next. This app will not have a custom launcher icon. It will use the blank activity template. Have the template create an activity named HelloMoonActivity. Adding Resources HelloMoon requires an image file and an audio file that are available in the solutions file for this book (http://www.bignerdranch.com/solutions/AndroidProgramming.zip). Within the solutions, find the following files: • 13_Audio/HelloMoon/res/drawable-mdpi/armstrong_on_moon.jpg • 13_Audio/HelloMoon/res/raw/one_small_step.wav For this simple app, we have created a single armstrong_on_moon.jpg file for screens of medium density (~160 dpi), which Android considers the baseline. Copy armstrong_on_moon.jpg to the drawable-mdpi directory. The audio file will be placed in the res/raw directory. The raw directory is a catch-all for resources that do not need special handling from Android’s build system. The res/raw directory is not created by default for a project, so you have to add it. (Right-click the res directory and select New → Folder.) Then copy one_small_step.wav to the new directory. 228 Adding Resources (You can also copy 13_Audio/HelloMoon/res/raw/apollo_17_stroll.mpg into res/raw/. There is information and a challenge at the end of this chapter about playing video that will use this file.) While you are thinking about resources, open res/values/strings.xml and add the strings that HelloMoon requires: Listing 13.1 Adding strings (strings.xml)Solved? Crimes Date of crime: (Wondering why HelloMoon uses “Play” and “Stop” strings for buttons instead of icons? You will be localizing this app in Chapter 15. Using icons would make localization less fun.) Now that you have the necessary resources in place, let’s take a look at the overall plan for HelloMoon. Figure 13.3 Object diagram for HelloMoon HelloMoon will have one activity, HelloMoonActivity, that hosts a HelloMoonFragment. is a class that you will write to encapsulate the MediaPlayer. Encapsulating the is not required; you could have HelloMoonFragment interact directly with MediaPlayer. But this design helps keep your code nice and separate. AudioPlayer MediaPlayer 229 Chapter 13 Audio Playback Using MediaPlayer Before you create the AudioPlayer class, you are going to set up the rest of the app. By now, you are familiar with these steps: • define a layout for the fragment • create the fragment class • modify the activity and its layout to host the fragment Defining the layout for HelloMoonFragment Create a new Android XML layout file named fragment_hello_moon.xml. Make its root element a TableLayout. Use Figure 13.4 to flesh out fragment_hello_moon.xml. Figure 13.4 Layout diagram for HelloMoon works a lot like a LinearLayout. Instead of nesting LinearLayouts, you can arrange widgets using TableRows. TableLayout and TableRow can team up to make it easier to arrange views in neat columns. Here, TableLayout helps arrange your buttons side by side in equally sized columns. TableLayout Why is the ImageView not in a TableRow? Children of TableRow act like “cells” of a table. Your goal here is for the ImageView to span the entire screen. If it were the child of a TableRow, then TableLayout would try to make other cells in its column span the screen, too. As a direct child of the TableLayout, it can do what it likes, and leave the Buttons to act as side-by-side, equal-width columns. Notice that the TableRow widget does not have to declare height and width attributes. It uses the TableLayout’s width and height as well as every other attribute of the TableLayout. A nested LinearLayout, on the other hand, can offer more flexibility in how your widgets will appear. 230 Manually resetting the app theme Preview the layout in the graphical tool. What color background are you seeing? You selected Holo Dark for HelloMoon’s theme in the wizard. However, as of this writing, the wizard ignores your theme choice and always assigns a light theme. Let’s see how to fix that. (If you see a dark background, the wizard has been fixed and you will not need to manually reset the app theme as described in the following section.) Manually resetting the app theme An application’s theme is declared in the application element of the manifest: ... HelloMoon name="hello_world">Hello world! name="menu_settings">Settings name="hellomoon_play">Play name="hellomoon_stop">Stop name="hellomoon_description">Neil Armstrong stepping onto the moon... The android:theme attribute is not required; if a theme is not declared, then the device’s default will be used. You can see that the value given here is a reference to a resource – @style/AppTheme. In the package explorer, find and open res/values/styles.xml. Within the style element named AppBaseTheme, change the parent value to android:Theme. Listing 13.2 Modifying default styles file (res/values/styles.xml) Each style is defined by a Bazam – you are the button king. 396 For the More Curious: include and merge Figure 24.5 They all look the same! For the More Curious: include and merge Earlier in the chapter you used the include tag to include a row of RemoteButtons multiple times (into the same layout). It works in the obvious way:The above include will include the contents of the layout file whose resource ID is @layout/ some_partial_layout. You can use include to reduce duplication in a single layout, as you did earlier in this chapter, or you can use it to reduce duplication between layouts. If you have some chunk of a layout that occurs in two or more layouts, you can just create that chunk as a layout and include it wherever it is needed. Then when you need to update it, you only have to do it in a single place. There are two more things you should know about include. First, the layout that you include will go through the normal resource search based on the current device configuration. So you can use configuration qualifiers just like with any layout. Second, you can override the android:id and any android:layout_* attributes of the root element of the included layout by specifying them on the include tag itself. This allows you to include the same layout multiple times and use different attributes each time you include it. The merge element works with the include element. merge can be used as the root element of an included layout instead of an actual widget. When one layout includes another layout that has merge as its root element, the children of the merge element are directly included – they become children of the parent of the include element and the merge element is discarded. 397 Chapter 24 Styles And Includes This is simpler than it sounds. If it is confusing, just remember that the merge element is discarded. It is only there to satisfy the XML requirement that there be a single instance of the root element. Challenge: Style Inheritance Your Delete and Enter buttons are sitting modestly at the bottom of the screen with the same style as all the number buttons. These “action” buttons need a bolder style. For this small challenge, make those buttons look special. Create a new button style that inherits from your RemoteButton style and sets a bold text style attribute. Then make your bottom row use your new style for the first and third buttons. Creating a style that inherits from another is simple. There are two ways to do it. One is to set the parent attribute of your style to the name of the style you want to inherit from. The easier way is to just prefix your style name with your parent’s style, then a dot – e.g., ParentStyleName.MyStyleName. 398 25 XML Drawables And 9-Patches In the last chapter, you quickly built a TV remote control interface using advanced layout tips and tricks. It looks okay, but maybe a little drab because it uses the stock Android look and feel for all of its buttons. In this chapter, you will use two new tools to give it a completely unique look and feel. Figure 25.1 RemoteControl makeover Both of these tools are drawables. Android calls anything that is intended to be drawn to the screen a drawable, whether it is an abstract shape, a clever bit of code that subclasses the Drawable class, or a bitmap image. You have already seen one kind of drawable: BitmapDrawable, which wraps an image. In this chapter, you will see a few more kinds of drawables: state list drawables, shape drawables, layer list drawables, and nine patch drawables. Since the first three are usually defined in XML files, we group them together in the broader category of XML drawables. 399 Chapter 25 XML Drawables And 9-Patches XML Drawables Before you tackle any XML drawables, try this: use the android:background attribute to change the background color of your buttons. Listing 25.1 An attempt at changing the button’s color (values/styles.xml) You should see something like this: Figure 25.2 What happened? The 3-dimensional look of the buttons is gone. If you press one of them, you will find that it no longer changes its appearance when you press it, either. Why did all that happen from one innocent attribute change? The Button class is not much more than a View with a default style applied to it. The style comes from whatever theme you have selected, and 400 XML Drawables sets a Drawable as the background. That background drawable was responsible for the 3-D look and the appearance change. By the end of this chapter, you will know how to make your own drawables that are just as capable. You will start by creating colored shapes with ShapeDrawables in XML. Since XML drawables are not density specific, they are usually placed in the default drawable folder instead of a density-specific one. In the package explorer, create a default res/drawable directory. In this directory, create a new XML file named button_shape_normal.xml. Make its root element shape. (Why is this one “normal”? Because soon it will have a not-so-normal friend.) Listing 25.2 Making a shape drawable button (drawable/ button_shape_normal.xml) This file defines a rounded rectangle. The corners element specifies the corner radius for your rounded rectangle, and the gradient elements specify the direction and the start and end colors for a color gradient. You can use shape to create other shapes, such as ovals, lines, and rings, and give them different looks. Check out the documentation at http://developer.android.com/guide/topics/resources/ drawable-resource.html for details. In styles.xml, update your Button style to use your new Drawable as a background. Listing 25.3 Update button style (values/styles.xml) Run RemoteControl and see the difference. 401 Chapter 25 XML Drawables And 9-Patches Figure 25.3 Now with rounded-corner buttons State List Drawables As good as they look, these buttons are still static. Under the covers, the old Button background was using a state list drawable. State list drawables allow you to display a different drawable for each state its associated View is in. (You previously used a state list drawable to change the background of list items in Chapter 18.) There are a lot of different states, but here you are only interested in whether the button is being pressed or not. Start off by creating the look for your pressed button. This look will be same as the normal button look except for the color. In the package explorer, make a copy of button_shape_normal.xml and name it button_shape_pressed.xml. Open button_shape_pressed.xml and add 180 degrees to the angle to switch the direction of the gradient: 402 State List Drawables Listing 25.4 Creating the shape for a pressed button (drawable/ button_shape_pressed.xml) Next you need a state list drawable. A state list drawable must have a selector root element and one or more item elements that describe different states. Right-click res/drawable/, create a new XML file named button_shape.xml, and give it a selector root element. Listing 25.5 Creating an interactive button shape (drawable/ button_shape.xml) The unpressed button has dark text, which used to work well against a light background. Now that the background is darker, a light color will look better when the button is unpressed, and you can use the dark color with the pressed background. You can create state list colors as well as state list shapes, so this is easy to do. Right-click res/drawable/ and create another state list drawable named button_text_color.xml. Listing 25.6 State-sensitive button text color (drawable/ button_text_color.xml) Now, in styles.xml, change your Button style to point to your new background drawable and your new text color. 403 Chapter 25 XML Drawables And 9-Patches Listing 25.7 Updating the button style (values/styles.xml) Run RemoteControl. Check out the new look of your pressed button. Figure 25.4 A pressed button Layer List and Inset Drawables The stock Android buttons you saw in the initial version of your app had a shadow effect. Unfortunately, shape drawables have no shadow property. But you can make the shadow effect yourself using two other XML drawable types: layer list drawables and inset drawables. Here is how you will do it. First, you will create a shadow with the same shape as your current button drawable. Then you will combine it with the current button using layer-list, and offset the bottom edge of the button a tad using inset so that the shadow becomes visible underneath. 404 Layer List and Inset Drawables Create a new XML file in res/drawable/. Name it button_shape_shadowed.xml and make its root element layer-list. Listing 25.8 Normal button shadows (drawable/ button_shape_shadowed.xml) The layer list contains multiple Drawables, ordered from back to front as they will be drawn on the screen. The second drawable in your list is an inset drawable. All it does is inset the bottom of the drawable you already created by 5dp, so that it sits above the shadow you created. Notice also that instead of using separate files for your shadow drawable, you directly embedded it in the layer list. This technique works just as well for other drawables, too, like your state list drawable above. Whether to nest your drawables or break them out into their own files as you did earlier is a judgment call. Breaking them out can reduce code duplication and simplify each file, but it also litters your drawable/ folder with additional files. Write whatever you find easiest to read and understand. In styles.xml, change your button style to point at the new shadowed drawable. Listing 25.9 Change button style one last time (values/styles.xml) One last change: create a gradient drawable for the entire main view, for a subtle lighting effect. Create a new drawable file named remote_background.xml with a shape root element. Then add the following to it. (Note: if a shape is not specified, rectangle is assumed.) 405 Chapter 25 XML Drawables And 9-Patches Listing 25.10 New background drawable for the root view (drawable/ remote_background.xml) Edit the fragment_remote_control.xml file to include the new drawable. Listing 25.11 Applying the drawable to the layout (layout/ fragment_remote_control.xml) Throughout all of these changes, you haven’t done anything to the fragment layout file. (Except for that last change, and of course you could have made a new style for the TableLayout itself.) This is good, since you aren’t changing the content of the layout, but its presentation. Adding new buttons or moving things around would require you to update the layout file, but changes to the look you can do in the style instead. Using 9-Patch Images If you enlist a professional designer to craft your UI, what they give you may not be achievable with XML drawables. It may still be possible to reuse the rendered asset in multiple places, though. For stretchable UI elements like button backgrounds, Android provides a tool called the 9-patch. Your next task is to fix up the two TextViews at the top of the screen. You will keep your button layout the same, but use stretchable drawables for their backgrounds. Both of them are much narrower than your TextViews, so that they do not take up much space. The first one, window.png, is a stretchable background designed to give your TextView a tasteful fade, along with some border space on the edges. 406 Using 9-Patch Images Figure 25.5 Channel display window The other, bar.png, gives a small shadow at the bottom and a thin border at the top. We have zoomed in on it quite a bit – it is pretty small. Figure 25.6 Channel entry area You can find these images in the solutions file in the 25_XMLDrawables/RemoteControl/res/ drawable-hdpi directory. Copy them into RemoteControl’s /res/drawable-hdpi folder. Modify the fragment_remote_control.xml file to include these images as backgrounds for the appropriate views. Change the text colors to something more appropriate to the new backgrounds, too – switch the top one to white, and switch the bottom one back to the default, black. And then set the bottom TextView to italics, because it looks neat. 407 Chapter 25 XML Drawables And 9-Patches Listing 25.12 Adding the drawables to the styles (layout/ fragment_remote_control.xml) can now be wired up to support this new button and ensure that it is enabled at the appropriate time. RunFragment Listing 36.6 Wiring up the Map button (RunFragment.java) public class RunFragment extends Fragment { ... private RunManager mRunManager; private Run mRun; private Location mLastLocation; private Button mStartButton, mStopButton, mMapButton; private TextView mStartedTextView, mLatitudeTextView, mLongitudeTextView, mAltitudeTextView, mDurationTextView; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... mMapButton = (Button)view.findViewById(R.id.run_mapButton); mMapButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent(getActivity(), RunMapActivity.class); i.putExtra(RunMapActivity.EXTRA_RUN_ID, mRun.getId()); startActivity(i); } }); updateUI(); } return view; ... 577 Chapter 36 Using Maps private void updateUI() { boolean started = mRunManager.isTrackingRun(); boolean trackingThisRun = mRunManager.isTrackingRun(mRun); if (mRun != null) mStartedTextView.setText(mRun.getStartDate().toString()); int durationSeconds = 0; if (mRun != null && mLastLocation != null) { durationSeconds = mRun.getDurationSeconds(mLastLocation.getTime()); mLatitudeTextView.setText(Double.toString(mLastLocation.getLatitude())); mLongitudeTextView.setText(Double.toString(mLastLocation.getLongitude())); mAltitudeTextView.setText(Double.toString(mLastLocation.getAltitude())); mMapButton.setEnabled(true); } else { mMapButton.setEnabled(false); } mDurationTextView.setText(Run.formatDuration(durationSeconds)); } mStartButton.setEnabled(!started); mStopButton.setEnabled(started && trackingThisRun); With this, RunTracker can now show you a map and your location on it. Run the app, load up a run, and press the Map button. You should see something like Figure 36.1, provided you have Internet access and you are near the Big Nerd Ranch office. Figure 36.1 RunTracker knows where you are 578 Displaying a Run’s Path Displaying a Run’s Path Your next task is to show the user a line that follows his or her run. The Maps API makes this trivial, but you need to fetch the list of locations so you know how to build the line. Add a method to both RunDatabaseHelper and RunManager to provide a LocationCursor with this data. Listing 36.7 Querying a run’s locations (RunDatabaseHelper.java) public LocationCursor queryLocationsForRun(long runId) { Cursor wrapped = getReadableDatabase().query(TABLE_LOCATION, null, COLUMN_LOCATION_RUN_ID + " = ?", // Limit to the given run new String[]{ String.valueOf(runId) }, null, // group by null, // having COLUMN_LOCATION_TIMESTAMP + " asc"); // order by timestamp return new LocationCursor(wrapped); } The queryLocationsForRun(long) method is very similar to the queryLastLocationForRun(long) method from the SQLite chapter, but it orders them in ascending order and returns them all. You can use this method in RunManager to give a nice façade to RunMapFragment. Listing 36.8 Querying a run’s locations, part II (RunManager.java) public LocationCursor queryLocationsForRun(long runId) { return mHelper.queryLocationsForRun(runId); } RunMapFragment can now use this new method to load the locations. Naturally, you will want to use a Loader to keep the database query off the main thread. Create LocationListCursorLoader as a new class to handle the job. Listing 36.9 A loader for locations (LocationListCursorLoader.java) public class LocationListCursorLoader extends SQLiteCursorLoader { private long mRunId; public LocationListCursorLoader(Context c, long runId) { super(c); mRunId = runId; } } @Override protected Cursor loadCursor() { return RunManager.get(getContext()).queryLocationsForRun(mRunId); } 579 Chapter 36 Using Maps Make use of this new loader in RunMapFragment to load the locations. Listing 36.10 Loading locations in RunMapFragment (RunMapFragment.java) public class RunMapFragment extends SupportMapFragment implements LoaderCallbacksCheck out what happens when you run the app: 408 Using 9-Patch Images Figure 25.7 Shattered dreams Each image was evenly stretched in all dimensions to fill the view. In some cases that is exactly what you want, but not here. Your images are all fuzzed out, and your bottom TextView’s digits are not properly centered. You can use 9-patch images to fix this problem. A 9-patch image file is specially formatted so that Android knows which portions can and cannot be scaled. Done properly, this ensures that the edges and corners of your background remain consistent with the image as it was created. Why are they called 9-patches? A 9-patch breaks your image into a 3 x 3 grid – a grid with 9 sections, or patches. The corners of the grid remain unscaled, the sides are only scaled in one dimension, and the center is scaled in both dimensions. Figure 25.8 What a 9-patch does 409 Chapter 25 XML Drawables And 9-Patches A 9-patch image is like a regular png image in everything except two aspects: its filename ends with .9.png, and it has an additional one pixel border around the edge. This border is used to specify the location of the center square of the nine patch. Border pixels are drawn black to indicate the center and transparent to indicate the edges. You can create a 9-patch using any image editor, but it is easier to use the draw9patch tool provided as part of the Android SDK. You can find it in the tools directory of your SDK installation. Once it’s running, you can drag files into it or open them from the File menu. Once the file is open, fill in black pixels on the top and left borders to mark the stretchable regions of the image. Add pixels to window.png as seen below. Figure 25.9 Channel display window 9-patch The top and left borders mark the stretchable region of the image. What about the bottom and right borders? They define an optional drawable region for the 9-patch image. The drawable region is the area where content (usually text) should be rendered. If you do not include a drawable region, it defaults to be the same as your stretchable region. This is what you want here, so you omit the drawable region. When you are done, save your results to a file called window_patch.9.png. Be careful that your 9patch files do not conflict with your other images – your application would fail to build if you had one file named window.9.png and another named window.png. Next, edit a 9-patch for bar.png. Use the drawable region here to center your text properly vertically and horizontally, as well as provide for a 4 pixel margin on all sides. 410 Using 9-Patch Images Figure 25.10 Channel entry area 9-patch When you finish, save out to bar_patch.9.png. Next, right-click your res/ folder and click Refresh. Eclipse lives in its own little world, and Refresh reminds it to wake up and look at the file system again. Finally, switch your two TextViews over to use your 9-patch drawables instead of the plain old images. Listing 25.13 Switch to use 9-patches (layout/ fragment_remote_control.xml) ... Run your app. Voilà! You now have working image backgrounds. Remember how plain your initial interface looked? It didn’t take a whole lot of time to create something far more interesting. Imagine 411 Chapter 25 XML Drawables And 9-Patches what you will be able to do with superior design work. Making your app a pleasure to look at makes it feel fine to use, and that will pay off in popularity. Figure 25.11 RemoteControl made over 412 26 HTTP & Background Tasks The apps that dominate the brains of users are networked apps. Those people fiddling with their phones instead of talking to each other at dinner? They are maniacally checking their newsfeeds, responding to text messages, or playing networked games. To get started with networking in Android, you are going to create a new app called PhotoGallery. PhotoGallery is a client for the photo-sharing site Flickr. It will fetch and display the most recent public photos uploaded to Flickr. Figure 26.1 gives you an idea of what the app will look like. Figure 26.1 Complete PhotoGallery (Figure 26.1 shows photos that are ours instead of public photos from Flickr. The photos on Flickr are the property of the person who posted them and may not be re-used without that person’s permission. To read more about permissions for using third-party content that you retrieve from Flickr, visit http://pressroom.yahoo.net/pr/ycorp/permissions.aspx.) 413 Chapter 26 HTTP & Background Tasks You will spend six chapters with PhotoGallery. It will take two chapters for you to get the basics of downloading and parsing XML and displaying images up and running. Once that is done, in subsequent chapters you will add on additional features that explore search, services, notifications, broadcast receivers, and web views. In this chapter, you will learn how to use Android’s high-level HTTP networking. Almost all day-today programming of web services these days is based on the HTTP networking protocol. By the end of the chapter, you will be fetching, parsing, and displaying photo captions from Flickr. (Retrieving and displaying photos will happen in Chapter 27.) Figure 26.2 PhotoGallery at the end of the chapter Creating PhotoGallery Create a new Android application project. Configure the app as shown in Figure 26.3. 414 Creating PhotoGallery Figure 26.3 Creating PhotoGallery Then have the wizard create a blank activity named PhotoGalleryActivity. PhotoGallery will follow the same architecture you have been using so far. PhotoGalleryActivity will be a SingleFragmentActivity subclass and its view will be the container view defined in activity_fragment.xml. This activity will host a fragment – in particular, an instance of PhotoGalleryFragment, which you will create shortly. Copy SingleFragmentActivity.java and activity_fragment.xml into your project from a previous project. In PhotoGalleryActivity.java, set up PhotoGalleryActivity as a SingleFragmentActivity by deleting the code that the template generated and replacing it with an implementation of createFragment(). Have createFragment() return an instance of PhotoGalleryFragment. (Bear with the error that this code will cause for the moment. It will go away after you create the PhotoGalleryFragment class.) Listing 26.1 Activity setup (PhotoGalleryActivity.java) public class PhotoGalleryActivity extends Activity { public class PhotoGalleryActivity extends SingleFragmentActivity { /* Auto-generated template code */ } @Override public Fragment createFragment() { return new PhotoGalleryFragment(); } 415 Chapter 26 HTTP & Background Tasks PhotoGallery will display its results in a GridView. This GridView will comprise the view for PhotoGalleryFragment. is an AdapterView, so it works much like ListView. However, unlike ListView, GridView has no handy dandy GridFragment to wire everything up for you. Which means that you will need to create a layout file and inflate it in PhotoGalleryFragment. Later in the chapter, you will wire up an adapter in PhotoGalleryFragment to feed the GridView photo captions to display. GridView To create a layout for the fragment, rename layout/activity_photo_gallery.xml to layout/ fragment_photo_gallery.xml. Then replace its contents with the GridView shown in Figure 26.4. Figure 26.4 A GridView (layout/fragment_photo_gallery.xml) Here you set the width of the columns to 120dp and instruct the GridView to create as many columns as will fit on the screen. If there is a leftover chunk of space less than 120dp, the stretchMode attribute tells the GridView to divide the extra space equally among the columns. Finally, create the PhotoGalleryFragment class. Retain the fragment, inflate the layout you just created, and get a reference to the GridView (Listing 26.2). 416 Networking Basics Listing 26.2 Some skeleton code (PhotoGalleryFragment.java) package com.bignerdranch.android.photogallery; ... public class PhotoGalleryFragment extends Fragment { GridView mGridView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } setRetainInstance(true); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false); mGridView = (GridView)v.findViewById(R.id.gridView); } } return v; Fire up PhotoGallery to make sure everything is wired up correctly before moving on. If all is well, you will be the proud owner of a blank screen. Networking Basics You are going to have one class handle the networking in PhotoGallery. Create a new Java class. Since you will be connecting to Flickr, name this class FlickrFetchr. FlickrFetchr will start off small with only two methods: getUrlBytes(String) and getUrl(String). The getUrlBytes(String) method fetches raw data from a URL and returns it as an array of bytes. The getUrl(String) method converts the result from getUrlBytes(String) to a String. In FlickrFetchr.java, add implementations for getUrlBytes(String) and getUrl(String) (Listing 26.3). 417 Chapter 26 HTTP & Background Tasks Listing 26.3 Basic networking code (FlickrFetchr.java) package com.bignerdranch.android.photogallery; ... public class FlickrFetchr { byte[] getUrlBytes(String urlSpec) throws IOException { URL url = new URL(urlSpec); HttpURLConnection connection = (HttpURLConnection)url.openConnection(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = connection.getInputStream(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { return null; } } } int bytesRead = 0; byte[] buffer = new byte[1024]; while ((bytesRead = in.read(buffer)) > 0) { out.write(buffer, 0, bytesRead); } out.close(); return out.toByteArray(); } finally { connection.disconnect(); } public String getUrl(String urlSpec) throws IOException { return new String(getUrlBytes(urlSpec)); } This code creates a URL object from a string – like, say, http://www.google.com. Then it calls openConnection() to create a connection object pointed at the URL. URL.openConnection() returns a URLConnection, but since you are connecting to an http URL, you can cast it to HttpURLConnection. This gives you HTTP-specific interfaces for working with request methods, response codes, streaming methods, and more. HttpURLConnection represents a connection, but it will not actually connect to your endpoint until you call getInputStream() (or getOutputStream() for POST calls). Until then, you cannot get a valid response code. Once you create your URL and open a connection, you call read() repeatedly until your connection runs out of data. The InputStream will yield bytes as they are available. When you are done, you close it and spit out your ByteArrayOutputStream’s byte array. While getUrlBytes(String) does the heavy lifting, getUrl(String) is what you will actually use in this chapter. It converts the bytes fetched by getUrlBytes(String) into a String. Right now, it may seem strange to split this work into two methods. However, having two methods will be useful in the next chapter when you start downloading image data. 418 Asking permission to network Asking permission to network One other thing is required to get networking up and running: you have to ask permission. Just as users would not want you secretly taking their pictures, they also do not want you to secretly download pictures of ASCII farm animals. To ask permission to network, add the following permission to your AndroidManifest.xml. Listing 26.4 Add networking permission to manifest (AndroidManifest.xml) ... Using AsyncTask to Run on a Background Thread The next step is to call and test the networking code you just added. However, you cannot simply call FlickrFetchr.getURL(String) directly in PhotoGalleryFragment. Instead, you need to create a background thread and run your code there. The easiest way to work with a background thread is with a utility class called AsyncTask. AsyncTask creates a background thread for you and runs the code in the doInBackground(…) method on that thread. In PhotoGalleryFragment.java, add a new inner class called FetchItemsTask at the bottom of PhotoGalleryFragment. Override AsyncTask.doInBackground(…) to get data from a website and log it. Then use the new class inside of PhotoGalleryFragment.onCreate(…). 419 Chapter 26 HTTP & Background Tasks Listing 26.5 Writing an AsyncTask (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { private static final String TAG = "PhotoGalleryFragment"; GridView mGridView; ... } private class FetchItemsTask extends AsyncTask ... { @Override protected Void doInBackground(Void... params) { try { String result = new FlickrFetchr().getUrl("http://www.google.com"); Log.i(TAG, "Fetched contents of URL: " + result); } catch (IOException ioe) { Log.e(TAG, "Failed to fetch URL: ", ioe); } return null; } } Now, in PhotoGalleryFragment.onCreate(…), call execute() on a new instance of FetchItemsTask. Listing 26.6 Writing an AsyncTask (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { private static final String TAG = "PhotoGalleryFragment"; GridView mGridView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } setRetainInstance(true); new FetchItemsTask().execute(); ... } The call to execute() will start your AsyncTask, which will then fire up its background thread and call doInBackground(…). Run your code and you should see Google’s Javascriptlicious home page HTML pop up in LogCat, looking something like Figure 26.5: Figure 26.5 Google HTML in LogCat 420 You and Your Main Thread Now that you have created a background thread and run some networking code on it, let’s take a closer look at threads in Android. You and Your Main Thread Networking does not happen immediately. A web server may take as long as a second or two to respond, and a file download can taken even longer than that. Because networking can take so long, Android disallows all networking on the main thread in Honeycomb and later versions of Android. If you try to do it, Android will throw a NetworkOnMainThreadException. Why? To understand that, you need to understand what a thread is, what the main thread is, and what the main thread does. A thread is a single sequence of execution. Code running within a single thread will execute one step after another. Every Android app starts life with a main thread. The main thread, however, isn’t a preordained list of steps. Instead, it sits in an infinite loop and waits for events initiated by the user or the system. Then it executes code in response to those events as they occur. Figure 26.6 Regular threads vs. the main thread Imagine that your app is an enormous shoe store, and that you only have one employee – The Flash. (Who hasn’t dreamed of that?) There are a lot of things to do in a store to keep the customers happy – arranging the merchandise, fetching shoes for customers, wielding the Brannock device. With The Flash as your salesperson, everyone is taken care of in a timely fashion, even though there is only one guy doing all the work. For this situation to work, The Flash cannot spend too much time doing any one thing. What if a shipment of shoes goes missing? Someone will have to spend a lot of time on the phone straightening it out. Your customers will get mighty impatient waiting for shoes while The Flash is on hold. The Flash is like the main thread in your application. It runs all the code that updates the UI. This includes the code executed in response to different UI-related events – activity startup, button presses, and so on. (Because the events are all related to the user interface in some way, the main thread is sometimes called the UI thread.) 421 Chapter 26 HTTP & Background Tasks The event loop keeps the UI code in sequence. It makes sure that none of these operations step on each other while still ensuring that the code is executed in a timely fashion. So far, all of the code you have written (except for the code you just wrote with AsyncTask) has been executed on the main thread. Beyond the main thread Networking is a lot like a phone call to your shoe distributor: it takes a long time compared to other tasks. During that time, the user interface will be completely unresponsive, which might result in an application not responding, or ANR. An ANR occurs when Android’s watchdog determines that the main thread has failed to respond to an important event, like pressing the back button. To the user, it looks like this: Figure 26.7 Application not responding This is why Android disallowed network operations on the main thread starting with Honeycomb. In your store, you would solve the problem by (naturally) hiring a second Flash to call the shoe distributor. In Android, you do something similar – you create a background thread and access the network from there. And what is the easiest way to work with a background thread? Why, AsyncTask. You will get to see other things AsyncTask can do later this chapter. Before you do that, you will want to do some real work with your networking code. Fetching XML From Flickr Flickr offers a fine XML API. All the details you need are available in the documentation at www.flickr.com/services/api/. Pull it up in your favorite web browser, and then find the list 422 Fetching XML From Flickr of Request�Formats. You will be using the simplest – REST. This tells us that the API endpoint is http://api.flickr.com/services/rest/. You can invoke the methods Flickr provides on this endpoint. Back on the main page of the API documentation, find the list of API�Methods. Scroll down to the photos section and locate flickr.photos.getRecent. Click on flickr.photos.getRecent, and the documentation will report that this method “Returns a list of the latest public photos uploaded to flickr.” That is exactly what you need for PhotoGallery. The only required parameter for the getRecent method is an API key. To get an API key, return to http://www.flickr.com/services/api/ and follow the link for API�keys. You will need a Yahoo ID to login. Once you are logged in, request a new, non-commercial API key. This usually only takes a moment. Your API key will look something like 4f721bgafa75bf6d2cb9af54f937bb70. Once you have a key, you have all you need to make a request to the Flickr web service. This request will be a GET request to http://api.flickr.com/services/rest/? method=flickr.photos.getRecent&api_key=xxx. Time to start coding. First, add some constants to FlickrFetchr. Listing 26.7 Add constants (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "FlickrFetchr"; private private private private static static static static final final final final String String String String ENDPOINT = "http://api.flickr.com/services/rest/"; API_KEY = "yourApiKeyHere"; METHOD_GET_RECENT = "flickr.photos.getRecent"; PARAM_EXTRAS = "extras"; private static final String EXTRA_SMALL_URL = "url_s"; These constants define the endpoint, the method name, the API key, and one extra parameter called extras, with a value of url_s. Specifying the url_s extra tells Flickr to include the URL for the small version of the picture if it is available. Now use the constants to write a method that builds an appropriate request URL and fetches its contents. 423 Chapter 26 HTTP & Background Tasks Listing 26.8 Add fetchItems() method (FlickrFetchr.java) public class FlickrFetchr { ... String getUrl(String urlSpec) throws IOException { return new String(getUrlBytes(urlSpec)); } } public void fetchItems() { try { String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_GET_RECENT) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .build().toString(); String xmlString = getUrl(url); Log.i(TAG, "Received xml: " + xmlString); } catch (IOException ioe) { Log.e(TAG, "Failed to fetch items", ioe); } } Here you use a Uri.Builder to build the complete URL for your Flickr API request. Uri.Builder is a convenience class for creating properly escaped parameterized URLs. Uri.Builder.appendQueryParameter(String,String) will automatically escape query strings for you. Finally, modify the AsyncTask in PhotoGalleryFragment to call the new fetchItems() method. Listing 26.9 Call fetchItems() (PhotoGalleryFragment.java) private class FetchItemsTask extends AsyncTask { @Override protected Void doInBackground(Void... params) { try { String result = new FlickrFetchr().getUrl("http://www.google.com"); Log.i(TAG, "Fetched contents of URL: " + result); } catch (IOException ioe) { Log.e(TAG, "Failed to fetch URL: ", ioe); } new FlickrFetchr().fetchItems(); return null; } } Run PhotoGallery and you should see rich, fertile Flickr XML in LogCat. 424 Fetching XML From Flickr Figure 26.8 Flickr XML Now that you have such fine XML from Flickr, what should you do with it? You do what you do with all data – put it in one or more model objects. The model class you are going to create for PhotoGallery is called GalleryItem. Figure 26.9 shows an object diagram of PhotoGallery. Figure 26.9 Object diagram of PhotoGallery 425 Chapter 26 HTTP & Background Tasks Note that Figure 26.9 does not show the hosting activity so that it can focus on the fragment and the networking code. Create the GalleryItem class and add the following code: Listing 26.10 Create model object class (GalleryItem.java) package com.bignerdranch.android.photogallery; public class GalleryItem { private String mCaption; private String mId; private String mUrl; } public String toString() { return mCaption; } Have Eclipse generate getters and setters for mId, mCaption, and mUrl. Now that you have made model objects, it is time to fill them with data from the XML you got from Flickr. To get the data from the XML, you will use the XmlPullParser interface. Using XmlPullParser XmlPullParser is an interface you can use to pull parse events off of a stream of XML. XmlPullParser is used internally by Android to inflate your layout files. It will work just parse your GalleryItem objects. as well to In FlickrFetchr add a constant that specifies the name of the photo XML element. Then write a method that uses XmlPullParser to identify each photo in the XML. Make a GalleryItem for each photo and add it to an ArrayList: 426 Using XmlPullParser Listing 26.11 Parse Flickr photos (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "FlickrFetchr"; private static final String ENDPOINT = "http://api.flickr.com/services/rest/"; private static final String API_KEY = "your API key"; private static final String METHOD_GET_RECENT = "flickr.photos.getRecent"; private static final String XML_PHOTO = "photo"; ... public void fetchItems() { ... } void parseItems(ArrayList items, XmlPullParser parser) throws XmlPullParserException, IOException { int eventType = parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && XML_PHOTO.equals(parser.getName())) { String id = parser.getAttributeValue(null, "id"); String caption = parser.getAttributeValue(null, "title"); String smallUrl = parser.getAttributeValue(null, EXTRA_SMALL_URL); } } } } GalleryItem item = new GalleryItem(); item.setId(id); item.setCaption(caption); item.setUrl(smallUrl); items.add(item); eventType = parser.next(); You can imagine XmlPullParser as having its finger on your XML document, walking step by step through different events like START_TAG, END_TAG, and END_DOCUMENT. At each step, you can call methods like getText(), getName(), or getAttributeValue(…) to answer any questions you have about the event XmlPullParser currently has its finger on. To move the finger to the next interesting event in the XML, call next(). Conveniently, this method also returns the type of event it just moved to. 427 Chapter 26 HTTP & Background Tasks Figure 26.10 How XmlPullParser works Your parseItems(…) method needs an XmlPullParser and an ArrayList. Get a parser instance and feed it the xmlString from Flickr. Then call parseItems(…) with the primed parser and empty array. Listing 26.12 Call parseItems(…) (FlickrFetchr.java) public void fetchItems() { public ArrayList fetchItems() { ArrayList items = new ArrayList (); try { String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_GET_RECENT) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .build().toString(); String xmlString = getUrl(url); Log.i(TAG, "Received xml: " + xmlString); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(new StringReader(xmlString)); } parseItems(items, parser); } catch (IOException ioe) { Log.e(TAG, "Failed to fetch items", ioe); } catch (XmlPullParserException xppe) { Log.e(TAG, "Failed to parse items", xppe); } return items; Run PhotoGallery to test your XML parsing code. PhotoGallery has no way of reporting the contents of your ArrayList right now, so you will need to set a breakpoint and use the debugger if you want to make sure everything worked correctly. 428 From AsyncTask Back to the Main Thread From AsyncTask Back to the Main Thread To finish off, let’s return to the view layer and get PhotoGalleryFragment’s GridView to display some captions. is an AdapterView, like ListView, so it needs an adapter to feed it views to display. GridView In PhotoGalleryFragment.java, add an ArrayList of GalleryItems and then set up an ArrayAdapter that uses a simple layout provided by Android. Listing 26.13 Implement setupAdapter() (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { private static final String TAG = "PhotoGalleryFragment"; GridView mGridView; ArrayList mItems; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false); mGridView = (GridView)v.findViewById(R.id.gridView); setupAdapter(); } return v; void setupAdapter() { if (getActivity() == null || mGridView == null) return; } if (mItems != null) { mGridView.setAdapter(new ArrayAdapter (getActivity(), android.R.layout.simple_gallery_item, mItems)); } else { mGridView.setAdapter(null); } Because GridView has no handy GridFragment class, you have to build your own adapter management code. One way of doing this is to use a method like the setupAdapter() method you just added. This method looks at the current model state and configures the adapter appropriately on your GridView. You want to call this in onCreateView(…), so that every time a new GridView is created on rotation, it is reconfigured with an appropriate adapter. You also want to call it every time your set of model objects changes. The android.R.layout.simple_gallery_item layout consists of a TextView element. Recall that in GalleryItem, you overrode toString() to return the object’s mCaption. So passing the array list of GalleryItems and this layout to the adapter is all you need for the GridView display captions. Notice that you check to see whether getActivity() is null before setting the adapter. Remember that fragments can exist unattached from any activity. Before now, this possibility has not come up because 429 Chapter 26 HTTP & Background Tasks your method calls have been driven by callbacks from the framework. If a fragment is receiving callbacks, then it definitely is attached to an activity. No activity, no callbacks. Now that you are using an AsyncTask, you are triggering some things yourself, and you cannot assume that the fragment is attached to an activity. So you must check to make sure that your fragment is still attached. If it is not, then operations that rely on the that activity (like creating your ArrayAdapter) will fail. Now you need to call setupAdapter() after data has been fetched from Flickr. Your first instinct might be to call setupAdapter() at the end of FetchItemsTask’s doInBackground(…). This is not a good idea. Remember that you have two Flashes in the store now – one helping multiple customers, and one on the phone with Flickr. What will happen if the second Flash tries to help customers after hanging up the phone? Odds are good that the two Flashes will step on each other’s toes. On a computer, this toe-stepping-on results in objects in memory becoming corrupted. Because of this, you are not allowed to update the UI from a background thread, nor is it safe or advisable to do so. What to do? AsyncTask has another method you can override called onPostExecute(…). onPostExecute(…) is run after doInBackground(…) completes. In addition, onPostExecute(…) is run on the main thread, not the background thread, so it is safe to update the UI within it. Modify FetchItemsTask to update mItems and call setupAdapter() after fetching your photos. Listing 26.14 Add adapter update code (PhotoGalleryFragment.java) private class FetchItemsTask extends AsyncTask { private class FetchItemsTask extends AsyncTask > { @Override protected Void doInBackground(Void... params) { protected ArrayList doInBackground(Void... params) { new FlickrFetchr().fetchItems(); return new FlickrFetchr().fetchItems(); return null; } } @Override protected void onPostExecute(ArrayList items) { mItems = items; setupAdapter(); } You made three changes here. First, you changed the type of the FetchItemsTask’s third generic parameter. This parameter is the type of result produced by your AsyncTask. It sets the type of value returned by doInBackground(…), as well as the type of onPostExecute(…)’s input parameter. Second, you modified doInBackground(…) to return your list of GalleryItems. By doing this you fix your code so that it compiles properly. You also pass your list of items off so that it may be used from within onPostExecute(…). Finally, you added an implementation of onPostExecute(…). This method accepts the list you fetched inside doInBackground(…), puts it in mItems, and updates your GridView’s adapter. With that, your work for this chapter is complete. Run, and you should see text displayed for each GalleryItem you downloaded. 430 For the More Curious: More on AsyncTask Figure 26.11 Your Flickr item captions For the More Curious: More on AsyncTask In this chapter you saw how to use the last type parameter on AsyncTask, which specifies the return type. What about the other two? The first type parameter allows you to specify the type of your input parameters. You would use it in the following way: AsyncTask task = new AsyncTask name="clear_search">Clear Search Then hook up your options menu callbacks. For the search button, you will call onSearchRequested() as described above. For the cancel button, for now you will do nothing. Listing 28.5 Options menu callbacks (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); } ... ... @Override public void onDestroyView() { ... } 454 Searchable activities @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_photo_gallery, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_search: getActivity().onSearchRequested(); return true; case R.id.menu_item_clear: return true; default: return super.onOptionsItemSelected(item); } } Run your new menu interface to see that it displays correctly. Figure 28.1 Your search interface Pressing the search button will not do anything right now, though. For onSearchRequested() to work, you must make PhotoGalleryActivity into a searchable activity. Searchable activities There are two moving pieces that make an activity searchable. The first is an XML file. This XML file contains an element called searchable that describes how the search dialog that you will show later 455 Chapter 28 Search should display itself. Create a new folder called res/xml and then create a new XML file in that folder called searchable.xml. Fill the file’s innards with a simple version of a searchable element: Listing 28.6 Search configuration (res/xml/searchable.xml)() { public Void doInBackground(String... params) { for (String parameter : params) { Log.i(TAG, "Received parameter: " + parameter); } }; } return null; task.execute("First parameter", "Second parameter", "Etc."); Input parameters are passed in to the execute(…) method, which takes in a variable number of arguments. Those variable arguments are then passed on to doInBackground(…). 431 Chapter 26 HTTP & Background Tasks The second type parameter allows you to specify the type for sending progress updates. Here is what that looks like: final ProgressBar progressBar = /* A determinate progress bar */; progressBar.setMax(100); AsyncTask name="search">Searchtask = new AsyncTask () { public Void doInBackground(Integer... params) { for (Integer progress : params) { publishProgress(progress); Thread.sleep(1000); } } }; public void onProgressUpdate(Integer... params) { int progress = params[0]; progressBar.setProgress(progress); } task.execute(25, 50, 75, 100); Progress updates usually happen in the middle of an ongoing background process. The problem is that you cannot make the necessary UI updates inside that background process. So AsyncTask provides publishProgress(…) and onProgressUpdate(…). Here is how it works: you call publishProgress(…) from doInBackground(…) in the background thread. This will make onProgressUpdate(…) be called on the UI thread. So you can do your UI updates in onProgressUpdate(…), but control them from doInBackground(…) with publishProgress(…). Cleaning Up AsyncTasks In this chapter, your AsyncTask was carefully structured so that you would not have to keep track of it. In other situations, though, you will need to keep a handle on your AsyncTasks, even canceling and rerunning them at times. For these more complicated uses, you will want to assign your AsyncTask to an instance variable. Once you have a handle on it, you can call AsyncTask.cancel(boolean). This method allows you to cancel an ongoing AsyncTask., AsyncTask.cancel(boolean) can work in a more rude or less rude fashion. If you call cancel(false), it will be polite and simply set isCancelled() to true. The AsyncTask check isCancelled() inside of doInBackground(…) and elect to finish prematurely. can then If you call cancel(true), however, it will be impolite and interrupt the thread doInBackground(…) is on, if it is currently running. AsyncTask.cancel(true) is a more severe way of stopping the AsyncTask. If you can avoid it, you should. 432 Challenge: Paging Challenge: Paging By default, getRecent returns one page of 100 results. There is an additional parameter you can use called page that will let you return page two, three, and so on. For this challenge, add code to your adapter that detects when you are at the end of your array of items and replaces the current page with the next page of results. For a slightly harder challenge, append subsequent pages to your results. 433 This page intentionally left blank 27 Loopers, Handlers, and HandlerThread Now that you have downloaded and parsed XML from Flickr, your next task is to download and display images. In this chapter, you will learn how to use Looper, Handler, and HandlerThread to dynamically download and display photos in PhotoGallery. Preparing GridView for Displaying Images The current adapter in PhotoGalleryFragment simply provides TextViews for the GridView to display. Each TextView displays the caption of a GalleryItem. To display photos, you need a custom adapter that provides ImageViews instead. Eventually, each ImageView will display a photo downloaded from the mUrl of a GalleryItem. Start by creating a new layout file for your gallery items called gallery_item.xml. This layout will consist of a single ImageView (Figure 27.1). Figure 27.1 Gallery item layout (res/layout/gallery_item.xml) These ImageViews will be managed by GridView, which means that their width will vary. Their height will remain fixed, though. To make the most of the ImageView’s space, you have set its scaleType to centerCrop. This setting centers the image and then scales it up so that the smaller dimension is equal to the view and the larger one is cropped on both sides. Next, you will need a placeholder image for each ImageView to display until you download an image to replace it. Find brian_up_close.jpg in the solutions file and put it in res/drawable-hdpi. 435 Chapter 27 Loopers, Handlers, and HandlerThread In PhotoGalleryFragment, replace the basic ArrayAdapter implementation with a custom ArrayAdapter whose getView(…) implementation returns an ImageView that displays the placeholder image. Listing 27.1 Create GalleryItemAdapter (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { ... void setupAdapter() { if (getActivity() == null || mGridView == null) return; } if (mItems != null) { mGridView.setAdapter(new ArrayAdapter (getActivity(), android.R.layout.simple_gallery_item, mItems)); mGridView.setAdapter(new GalleryItemAdapter(mItems)); } else { mGridView.setAdapter(null); } private class FetchItemsTask extends AsyncTask > { ... } private class GalleryItemAdapter extends ArrayAdapter { public GalleryItemAdapter(ArrayList items) { super(getActivity(), 0, items); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = getActivity().getLayoutInflater() .inflate(R.layout.gallery_item, parent, false); } ImageView imageView = (ImageView)convertView .findViewById(R.id.gallery_item_imageView); imageView.setImageResource(R.drawable.brian_up_close); } } } return convertView; Remember that the AdapterView (the GridView, in this case) calls getView(…) on its adapter for every individual view it needs. 436 Preparing GridView for Displaying Images Figure 27.2 AdapterView-ArrayAdapter pong Run PhotoGallery, and you should see an array of close-up Brians: Figure 27.3 A Briansplosion 437 Chapter 27 Loopers, Handlers, and HandlerThread Downloading Lots of Small Things Currently, PhotoGallery’s networking works like this: PhotoGalleryFragment executes an AsyncTask that retrieves the XML from Flickr on a background thread and parses the XML into an array of GalleryItems. Each GalleryItem now has a URL where a thumbnail-size photo lives. The next step is to go and get those thumbnails. You might think that this additional networking code could simply be added to FetchItemsTask’s doInBackground() method. Your GalleryItem array has 100 URLs to download from. You would download the images one after the other until you had all 100. When onPostExecute(…) executed, they would be displayed en masse in the GridView. However, downloading the thumbnails all at once causes two problems. The first is that it could take a while, and the UI would not be updated until the downloading was complete. On a slow connection, users would be staring at a wall of Brians for a long time. The second problem is the cost of having to store the entire set of images. One hundred thumbnails will fit into memory easily. But what if it were 1000? What if you wanted to implement infinite scrolling? Eventually, you would run out of space. Given these problems, real world apps often choose to download images only when they need to be displayed on screen. Downloading on demand puts the responsibility on the GridView and its adapter. The adapter will trigger the image downloading as part of its getView(…) implementation. is the easiest way to get a background thread, but it is fundamentally ill-suited for repetitive and long-running work. (You can read why in the For The More Curious section at the end of this chapter.) AsyncTask Instead of using an AsyncTask, you are going to create a dedicated background thread. This is the most common way to implement downloading on an as-needed basis. Communicating with the Main Thread Your dedicated thread will download photos, but how will it work with the GridView’s adapter to display them when it cannot directly access the main thread? Think back to the shoe store with two Flashes. Background Flash has wrapped up his phone call to the distributor. He needs to tell Main Flash that the shoes are back in stock. If Main Flash is busy, Background Flash cannot do this right away. He would have to wait by the register to catch Main Flash at a spare moment. This would work, but it is not very efficient. The better solution is to give each Flash an inbox. Background Flash writes a message about the shoes being in stock and puts it on top of Main Flash’s inbox. Main Flash does the same thing when he wants to tell Background Flash that the stock of shoes has run out. The inbox idea turns out to be really handy. The Flash may have something that needs to be done soon, but not right at the moment. In that case, he can put a message in his own inbox and then handle it when he has time. In Android, the inbox that threads use is called a message queue. A thread that works by using a message queue is called a message loop; it loops again and again looking for new messages on its queue (Figure 27.4). 438 Assembling a Background Thread Figure 27.4 Flash dance A message loop consists of a thread and a looper. The Looper is the object that manages a thread’s message queue. The main thread is a message loop and has a looper. Everything your main thread does is performed by its looper, which grabs messages off of its message queue and performs the task it specifies. You are going to create a background thread that is a message loop, too. You will use a class called HandlerThread that prepares a Looper for you. Assembling a Background Thread Create a new class called ThumbnailDownloader that extends HandlerThread. Your ThumbnailDownloader’s user will need to use some object to identify each download, so give it one generic argument called Token by naming it ThumbnailDownloader in the class creation dialog. Then give it a constructor and a stub implementation of a method called queueThumbnail() (Listing 27.2). Listing 27.2 Initial thread code (ThumbnailDownloader.java) public class ThumbnailDownloader extends HandlerThread { private static final String TAG = "ThumbnailDownloader"; public ThumbnailDownloader() { super(TAG); } } public void queueThumbnail(Token token, String url) { Log.i(TAG, "Got an URL: " + url); } 439 Chapter 27 Loopers, Handlers, and HandlerThread Notice that queueThumbnail() expects a Token and a String. This is the method you will have GalleryItemAdapter call in its getView(…) implementation. Open PhotoGalleryFragment.java. Give PhotoGalleryFragment a ThumbnailDownloader member variable. You can use any object as a token for ThumbnailDownloader. In this case, the ImageView makes for a convenient token, since that is where the downloaded images will eventually go. In onCreate(…), create the thread and start it. Override onDestroy() to quit the thread. Listing 27.3 Create ThumbnailDownloader (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { private static final String TAG = "PhotoGalleryFragment"; GridView mGridView; ArrayList mItems; ThumbnailDownloader mThumbnailThread; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); new FetchItemsTask().execute(); } mThumbnailThread = new ThumbnailDownloader (); mThumbnailThread.start(); mThumbnailThread.getLooper(); Log.i(TAG, "Background thread started"); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... } } @Override public void onDestroy() { super.onDestroy(); mThumbnailThread.quit(); Log.i(TAG, "Background thread destroyed"); } ... (A couple of safety notes: one, notice that you call getLooper() after calling start() on your ThumbnailDownloader. This is a way to ensure that the thread’s guts are ready before proceeding (you will learn more about the Looper in a moment). And two, you call quit() to terminate the thread inside onDestroy(). This is critical. If you do not quit your HandlerThreads, they will never die. Like zombies. Or rock and roll. Or rocks.) Finally, within GalleryItemAdapter.getView(…), retrieve the correct GalleryItem using the position parameter, call the thread’s queueThumbnail() method, and pass in the ImageView and the item’s URL. 440 Messages and Message Handlers Listing 27.4 Hook up ThumbnailDownloader (PhotoGalleryFragment.java) private class GalleryItemAdapter extends ArrayAdapter { ... @Override public View getView(int position, View convertView, ViewGroup parent) { ... ImageView imageView = (ImageView)convertView .findViewById(R.id.gallery_item_imageView); imageView.setImageResource(R.drawable.brian_up_close); GalleryItem item = getItem(position); mThumbnailThread.queueThumbnail(imageView, item.getUrl()); } } return convertView; Run PhotoGallery and check out LogCat. When you scroll around the GridView, you should see lines in LogCat signaling that ThumbnailDownloader is getting each one of your download requests. Now that you have a HandlerThread up and running, the next step is to create a message with the information passed in to queueThumbnail() and put that message on the ThumbnailDownloader’s message queue. Messages and Message Handlers Before you create a message, you need to understand what a Message is, and the relationship it has with its Handler, or message handler. Message anatomy Let’s start by looking closely at messages. The messages that a Flash might put in an inbox (its own inbox or that of another Flash) are not supportive notes, like "You run very fast, Flash." They are tasks that need to be handled. A message is an instance of Message and contains several fields. Three are relevant to your implementation: what a user-defined int that describes the message obj a user-specified object to be sent with the message target the Handler that will handle the message The target of a Message is an instance of Handler. You can think of the name Handler as being short for “message handler.” When you create a Message, it will automatically be attached to a Handler. And when your Message is ready to be processed, Handler will be the object in charge of making it happen. Handler anatomy So to do any real work with messages, you will need an instance of the target message Handler first. A Handler is not just a target for processing your Messages. A Handler is your interface for creating and posting Messages, too. 441 Chapter 27 Loopers, Handlers, and HandlerThread Figure 27.5 Looper, Handler, HandlerThread, and Message Messages must be posted and consumed on a Looper, because Looper owns the inbox of Message objects. So Handler always has a reference to its coworker, the Looper. A Handler is attached to exactly one Looper, and a Message is attached to exactly one target Handler, called its target. A Looper has a whole queue of Messages. Figure 27.6 Multiple Handlers, one Looper Multiple Handlers can be attached to one Looper. This means that your Handler’s Messages may be living side by side with another Handler’s messages. Using handlers Usually, you do not set a message’s target Handler by hand. It is better to build the message by calling Handler.obtainMessage(…). You pass the other message fields into this method, and it automatically sets the target for you. 442 Using handlers pulls from a common recycling pool to avoid creating new Message objects, so it is also more efficient than creating new instances. Handler.obtainMessage(…) Once you have obtained a Message, you then call sendToTarget() to send the Message to its Handler. The Handler will then put the Message on the end of Looper’s message queue. In this case, you are going to obtain a message and send it to its target within the implementation of queueThumbnail(). The message’s what will be a constant defined as MESSAGE_DOWNLOAD. The obj will be the Token – in this case, the ImageView that the adapter passed in to queueThumbnail(). When the looper gets to a particular message in the queue, it gives the message to the message’s target to handle. Typically, the message is handled in the target’s implementation of Handler.handleMessage(…). Figure 27.7 Creating a Message and sending it In this case, your implementation of handleMessage(…) will use FlickrFetchr to download bytes from the URL and then turn these bytes into a bitmap. Add the code shown in Listing 27.5. 443 Chapter 27 Loopers, Handlers, and HandlerThread Listing 27.5 Obtaining, sending, and handling a message (ThumbnailDownloader.java) public class ThumbnailDownloader extends HandlerThread { private static final String TAG = "ThumbnailDownloader"; private static final int MESSAGE_DOWNLOAD = 0; Handler mHandler; Map requestMap = Collections.synchronizedMap(new HashMap ()); public ThumbnailDownloader() { super(TAG); } @SuppressLint("HandlerLeak") @Override protected void onLooperPrepared() { mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MESSAGE_DOWNLOAD) { @SuppressWarnings("unchecked") Token token = (Token)msg.obj; Log.i(TAG, "Got a request for url: " + requestMap.get(token)); handleRequest(token); } } }; } public void queueThumbnail(Token token, String url) { Log.i(TAG, "Got a URL: " + url"); requestMap.put(token, url); } mHandler .obtainMessage(MESSAGE_DOWNLOAD, token) .sendToTarget(); private void handleRequest(final Token token) { try { final String url = requestMap.get(token); if (url == null) return; byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap = BitmapFactory .decodeByteArray(bitmapBytes, 0, bitmapBytes.length); Log.i(TAG, "Bitmap created"); } } } catch (IOException ioe) { Log.e(TAG, "Error downloading image", ioe); First, let’s talk about that annotation at the top of onLooperPrepared(), @SuppressLint("HandlerLeak"). Here, Android Lint will warn you about subclassing Handler. Your 444 Passing handlers will be kept alive by its Looper. So if your Handler is an anonymous inner class, it is easy to leak memory accidentally through an implicit object reference. Here, though, everything is tied to your HandlerThread, so there is no danger of leaking anything. Handler The other annotation, @SuppressWarnings("unchecked"), is more run of the mill. This is necessary because Token is a generic class argument, but Message.obj is an Object. Due to type erasure, it is not possible to actually make this cast. If you would like to know more about why, read up on type erasure – this chapter will stay focused on Android stuff. The requestMap variable is a synchronized HashMap. Here, using the Token as a key, you can store and retrieve the URL associated with a particular Token. In queueThumbnail(), you add the passed-in Token-URL pair to the map. Then you obtain a message, give it the Token as its obj, and send it off to be put on the message queue. You implemented Handler.handleMessage(…) in your Handler subclass within onLooperPrepared(). HandlerThread.onLooperPrepared() is called before the Looper checks the queue for the first time. This makes it a good place to create your Handler implementation. Within Handler.handleMessage(…), you check the message type, retrieve the Token and then pass it to handleRequest(…). The handleRequest() method is where the downloading happens. Here you check for the existence of a URL. Then you pass the URL to a new instance of your old friend FlickrFetchr. In particular, you use the FlickrFetchr.getUrlBytes(…) method that you created with such foresight in the last chapter. Finally, you use BitmapFactory to construct a bitmap with the array of bytes returned from getUrlBytes(…). Run PhotoGallery and check LogCat for your confirming log statements. Of course, the request will not be completely handled until you set the bitmap on the ImageView that originally came from GalleryItemAdapter. However, this is UI work, so it must be done on the main thread. Everything you have seen so far is using handlers and messages on a single thread – putting messages in your own inbox. In the next section, you will see how ThumbnailDownloader can use a Handler to access the main thread. Passing handlers One way that a HandlerThread can get work done on the main thread is to have the main thread pass its own Handler to the HandlerThread. The main thread is a message loop with handlers and a Looper. When you create a Handler in the main thread, it will be associated with the main thread’s Looper. You can then pass that Handler to another thread. The passed Handler maintains its loyalty to the Looper of the thread that created it. Any messages the Handler is responsible for will be handled on the main thread’s queue. So just like you were able to schedule work on the background thread from the main thread using ThumbnailDownloader’s Handler. 445 Chapter 27 Loopers, Handlers, and HandlerThread Figure 27.8 Scheduling work on ThumbnailDownloader from the main thread You can also schedule work on the main thread from the background thread using a Handler attached to the main thread: Figure 27.9 Scheduling work on the main thread from ThumbnailDownloader’s thread In ThumbnailDownloader.java, add the mResponseHandler variable seen above to hold a Handler passed from the main thread. Then replace the constructor with one that accepts a Handler and sets the variable, and add a listener interface to communicate the responses with. 446 Passing handlers Listing 27.6 Add response Handler (ThumbnailDownloader.java) public class ThumbnailDownloader extends HandlerThread { private static final String TAG = "ThumbnailDownloader"; private static final int MESSAGE_DOWNLOAD = 0; Handler mHandler; Map requestMap = Collections.synchronizedMap(new HashMap ()); Handler mResponseHandler; Listener mListener; public interface Listener { void onThumbnailDownloaded(Token token, Bitmap thumbnail); } public void setListener(Listener listener) { mListener = listener; } public ThumbnailDownloader() { super(TAG); public ThumbnailDownloader(Handler responseHandler) { super(TAG); mResponseHandler = responseHandler; } Then modify PhotoGalleryFragment to pass a Handler to ThumbnailDownloader, as well as a Listener to set the returning Bitmaps on the ImageView handles. Remember that by default, the Handler will attach itself to the Looper for the current thread. Since this Handler is created in onCreate(…), it will be attached to the main thread’s Looper. Listing 27.7 Hook up to response Handler (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); new FetchItemsTask().execute(); } mThumbnailThread = new ThumbnailDownloader(); mThumbnailThread = new ThumbnailDownloader (new Handler()); mThumbnailThread.setListener(new ThumbnailDownloader.Listener () { public void onThumbnailDownloaded(ImageView imageView, Bitmap thumbnail) { if (isVisible()) { imageView.setImageBitmap(thumbnail); } } }); mThumbnailThread.start(); mThumbnailThread.getLooper(); Log.i(TAG, "Background thread started"); Now ThumbnailDownloader has access via mResponseHandler to a Handler that is tied to the main thread’s Looper. It also has your Listener to do the UI work with the returning Bitmaps. Note that you 447 Chapter 27 Loopers, Handlers, and HandlerThread guard your call to imageView.setImageBitmap(Bitmap) with a call to Fragment.isVisible(). This ensures that you are not setting the image on a stale ImageView. You could send a custom Message back to the main thread this way. This would require another subclass of Handler, with an override of handleMessage(…). Instead, let’s use another handy Handler method – post(Runnable). Handler.post(Runnable) is a convenience method for posting Messages that look like this: Runnable myRunnable = new Runnable() { public void run() { /* Your code here */ } }; Message m = mHandler.obtainMessage(); m.callback = myRunnable; When a Message has its callback field set, instead of being run by its Handler target, the Runnable in callback is run instead. In ThumbnailDownloader.handleRequest(), add the following code. Listing 27.8 Downloading and displaying (ThumbnailDownloader.java) ... private void handleRequest(final Token token) { try { final String url = requestMap.get(token); if (url == null) return; byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap = BitmapFactory .decodeByteArray(bitmapBytes, 0, bitmapBytes.length); Log.i(TAG, "Bitmap created"); mResponseHandler.post(new Runnable() { public void run() { if (requestMap.get(token) != url) return; requestMap.remove(token); mListener.onThumbnailDownloaded(token, bitmap); } } }); } catch (IOException ioe) { Log.e(TAG, "Error downloading image", ioe); } And because mResponseHandler is associated with the main thread’s Looper, this UI update code will be run on the main thread. So what does this code do? First, it double-checks the requestMap. This is necessary because the GridView recycles its views. By the time ThumbnailDownloader finishes downloading the Bitmap, 448 For the More Curious: AsyncTask vs. Threads GridView may have recycled the ImageView and requested a different URL for it. This check ensures that each Token gets the correct image, even if another request has been made in the meantime. Finally, you remove the Token from the requestMap and set the bitmap on the Token. Before running and seeing your hard-won images, there is one last danger you need to account for. If the user rotates the screen, ThumbnailDownloader may be hanging on to invalid ImageViews. Bad things will happen if those ImageViews get pressed. Write the following method to clean all the requests out of your queue. Listing 27.9 Add cleanup method (ThumbnailDownloader.java) public void clearQueue() { mHandler.removeMessages(MESSAGE_DOWNLOAD); requestMap.clear(); } Then clean out your downloader in PhotoGalleryFragment when your view is destroyed. Listing 27.10 Call cleanup method (PhotoGalleryFragment.java) @Override public void onDestroyView() { super.onDestroyView(); mThumbnailThread.clearQueue(); } With that, your work on this chapter is complete. Run PhotoGallery. Scroll around to see images dynamically loading. PhotoGallery has achieved its basic goal of displaying images from Flickr. In the next few chapters, you will add more functionality like searching for photos and opening each photo’s Flickr page in a web view. For the More Curious: AsyncTask vs. Threads Now that you understand Handler and Looper, AsyncTask may not seem quite so magical. It is still less work than what you have done here. So why not use AsyncTask instead of a HandlerThread? There are a few reasons. The most fundamental one is that AsyncTask is not designed for it. It is intended for work that is short lived and not repeated too often. Your code in the previous chapter is a place where AsyncTask shines. If you are creating a lot of AsyncTasks, though, or having them run for a long time, you are probably using the wrong class. A more compelling technical reason is that in Android 3.2 AsyncTask changed its implementation in a significant way. Starting with Android 3.2, AsyncTask does not create a thread for each instance of AsyncTask. Instead, it uses something called an Executor to run background work for all AsyncTasks on a single background thread. That means that each AsyncTask will run one after the other. A longrunning AsyncTask will poison the well, preventing other AsyncTasks from getting any CPU time. It is possible to safely run AsyncTask in parallel by using a thread pool executor instead, but we do not recommend doing so. If you are considering doing this, it is usually better to do your own threading instead, using Handlers to communicate back to the main thread when necessary. 449 Chapter 27 Loopers, Handlers, and HandlerThread Challenge: Preloading and Caching Users accept that not everything can be instantaneous. (Well, most users.) Even so, programmers strive towards perfection. To approach instantaneity, most real world apps augment the code you have here in two ways: • adding a caching layer • preloading images A cache is a place to stash a certain number of Bitmap objects so that they stick around even when you are done using them. A cache can only hold so many items, so you need a strategy to decide what to keep when your cache runs out of room. Many caches use a strategy called LRU, or “least recently used.” When you are out of room, the cache gets rid of the least recently-used item. The Android support library has a class called LruCache that implements an LRU strategy. For the first challenge, use LruCache to add a simple cache to ThumbnailDownloader. Whenever you download the Bitmap for an URL, you will stick it in the cache. Then, when you are about to download a new image, you will check the cache first to see if you already have it around. Once you have built a cache, you can preload things into it. Preloading is loading items in the cache before you actually need them. That way, there is no delay for Bitmaps to download before displaying them. Preloading is tricky to implement well, but it makes a huge difference for the user. For a second, harder challenge, for every GalleryItem you display, preload Bitmaps for the previous ten and the next ten GalleryItems. 450 28 Search Your next task with PhotoGallery is to search photos on Flickr. In this chapter, you will learn how to integrate search into your app the Android way. Or the Android ways, as it turns out. Search has been integrated into Android from the very beginning, but (much like the menu button), it has changed a lot since then. Like menus, the new code for search was built on the existing APIs. So when you build search in the older style, you are preparing to implement the full-featured modern Jelly Bean search. Searching Flickr Let’s begin with the Flickr side of things. To search Flickr, you call the flickr.photos.search method. Here is what a flickr.photos.search method invocation to look for the text “red” looks like: http://api.flickr.com/services/rest/?method=flickr.photos.search &api_key=XXX&extras=url_s&text=red This method takes in some new and different parameters to specify what the search terms are, like a text query parameter. The good news is that parsing the XML you get back into GalleryItems works exactly the same. Make the changes in Listing 28.1 to add a new search request method to FlickrFetchr. Since both search and getRecent parse GalleryItems in the same way, you will refactor some of your old code from fetchItems() into a new method called downloadGalleryItems(String). Pay close attention – the old fetchItems() URL code is cut and pasted into a new version of the fetchItems() method, not deleted. 451 Chapter 28 Search Listing 28.1 Add Flickr search method (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "PhotoFetcher"; private private private private private private static static static static static static final final final final final final String String String String String String ENDPOINT = "http://api.flickr.com/services/rest/"; API_KEY = "4f721bbafa75bf6d2cb5af54f937bb70"; METHOD_GET_RECENT = "flickr.photos.getRecent"; METHOD_SEARCH = "flickr.photos.search"; PARAM_EXTRAS = "extras"; PARAM_TEXT = "text"; ... public ArrayList fetchItems() { public ArrayList downloadGalleryItems(String url) { ArrayList items = new ArrayList (); try { String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_GET_RECENT) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .build().toString(); String xmlString = getUrl(url); Log.i(TAG, "Received xml: " + xmlString); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(new StringReader(xmlString)); } parseItems(items, parser); } catch (IOException ioe) { Log.e(TAG, "Failed to fetch items", ioe); } catch (XmlPullParserException xppe) { Log.e(TAG, "Failed to parse items", xppe); } return items; public ArrayList fetchItems() { // Move code here from above String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_GET_RECENT) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .build().toString(); return downloadGalleryItems(url); } } 452 public ArrayList search(String query) { String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_SEARCH) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .appendQueryParameter(PARAM_TEXT, query) .build().toString(); return downloadGalleryItems(url); } The Search Dialog The downloadGalleryItems(String) method is used twice because the code to download and parse the URL is the same for both search and getRecent. Searching is simply a matter of hitting the new search method, flickr.photos.search, passing in the encoded query string as the text parameter. Next, hook up some test code to call your search code inside PhotoGalleryFragment.FetchItemsTask. For now, you will hardwire a search query just to make sure that it works. Listing 28.2 Hardwired search query code (PhotoGalleryFragment.java) private class FetchItemsTask extends AsyncTask > { @Override protected ArrayList doInBackground(Void... params) { String query = "android"; // Just for testing } } } if (query != null) { return new FlickrFetchr().search(query); } else { return new FlickrFetchr().fetchItems(); } @Override protected void onPostExecute(ArrayList items) { ... } ... The default will be to use the old getRecent code. If there is a non-null search query (which for now is always the case), then FetchItemsTask will fetch search results instead. Run PhotoGallery and see what you get. Hopefully, you will see an Andy or two. The Search Dialog In this section, you will implement Android’s search interface in PhotoGallery. You will start with the old style dialog interface. Creating a search interface In Honeycomb, the Android folks got rid of hardware search buttons. Even before that, though, there was no guarantee of having a search button. Modern Android apps that rely on search must always have a search button somewhere in the app if they are targeting pre-3.0 devices. Implementing this is not difficult. Simply call Activity.onSearchRequested(). This method performs the exact same operation as pressing the search button. Add a menu XML file for PhotoGallery in res/menu/fragment_photo_gallery.xml. Your app will also need an interface to clear the search query, so add a clear button as well. 453 Chapter 28 Search Listing 28.3 Add search menu items (res/menu/ fragment_photo_gallery.xml) You are missing a couple of strings now, so go ahead and add them to strings.xml. (Later on, you will need a search hint string, so add that as well.) Listing 28.4 Add search strings (res/values/strings.xml) ... PhotoGalleryActivity name="search_hint">Search FlickrThis XML is called a search configuration. In your case, all your search configuration needs to do is provide a hint string and the name of your application. In a fancier application, this file can become fat with instructions. Search suggestions, voice search, global search configuration, action key configurations, and input types all go here. Even in the simplest implementation, though, a basic search configuration is required. The next pieces of the puzzle go in AndroidManifest.xml. You need to change the launch mode of your app, and you need to declare an additional intent filter and a piece of metadata for PhotoGalleryActivity. The intent filter advertises that you can listen to search intents, and the metadata is to attach the XML you just wrote to your activity. All of this advertisement work is required to tell Android’s SearchManager that your activity is capable of handling searches and to give it your search configuration. SearchManager is an OS-level service that is responsible for presenting the search dialog and managing its interactions. Crack open AndroidManifest.xml and add the two elements and the android:launchMode attribute shown in Listing 28.7. Listing 28.7 Add intent filter and metadata (AndroidManifest.xml) ... 456 Searchable activities Let’s talk about the last two elements first. The first addition is a familiar intent filter definition. Search results are communicated by calling startActivity(…) with an intent that has an action of android.intent.action.SEARCH. The search query is put on the intent as an extra. So to indicate that your activity can handle search results, you define a filter for the action.intent.action.SEARCH intent. The second addition is another metadata tag. You used metadata earlier in Chapter 16, but this tag is slightly different. Instead of android:value, it uses android:resource. Here is an example to show you the difference. Let’s say that you referred to the same string resource in two different metadata tags:If you were to pull out the value for metadata.value, you would find that it contained the string “PhotoGallery”, which is the value stored in @string/app_name. The value for metadata.resource, though, would be the integer ID of that resource. In other words, the value of metadata.resource is the value in code of R.string.app_name. Back in reality, the SearchManager requires the integer ID of searchable.xml, not the string value of that XML file. So you use android:resource and give the SearchManager the file’s resource ID. What about the android:launchMode attribute on your activity tag? This is the activity’s launch mode. You will learn about this in a moment when you write the code to receive the search query. With that, you should be able to fire up a search dialog. Run PhotoGallery and tap your menu search button. Figure 28.2 The search dialog 457 Chapter 28 Search Hardware search button The same code that is invoked when you call onSearchRequested() by hand will be run when you press the hardware search button on older devices. If you would like to verify for yourself that this works, you can modify any pre-3.0 emulator to have a hardware search button by configuring your emulator to use the hardware keyboard, like so: Figure 28.3 Add hardware keyboard support How Search works The main idea behind search in Android is the concept mentioned briefly earlier, the searchable activity. A searchable activity is defined by the two things you just created: a search intent filter and a search configuration metadata entry. With the hardware search button, every search interaction up until the search intent itself is handled by the system. It checks your AndroidManifest.xml to see if your activity is searchable. If it is, it then shows a search dialog activity on top of your activity. That activity then triggers search by sending you a new intent. 458 Launch modes and new intents Figure 28.4 System search Which means that, normally, pressing the search button will start a new activity. In your case, though, it does not. Why? Because you added android:launchMode="singleTop" (Listing 28.7), which changes your launch mode. Launch modes and new intents What is a launch mode? Launch modes determine how your activity starts up when it receives a new intent, and sometimes how it behaves when it fires an intent to start another activity. Table 28.1 The different kinds of launch modes Launch mode Behavior standard The default behavior – start up a new activity for every new intent received. singleTop If an instance of this activity is already at the top of the back stack, direct the new intent at the existing activity instead of creating a new one. singleTask Launch this activity into its own task. If the activity already exists in the task, clear out any activities above it in the back stack and direct the new intent at the existing activity. singleInstance Launch this activity into its own task. It is the only activity in its task – if any other activities are started from this task, they are launched into their own tasks. If the activity already exists, direct the new intent at the existing activity. 459 Chapter 28 Search Every activity you have written so far has used the standard launch mode. This behavior is familiar: when an intent resolves to an activity with the standard launch mode, a new instance of that activity is created and added to the back stack. This behavior will not fly for PhotoGalleryActivity in all cases. (We will discuss why when we talk about SearchView and Honeycomb search later.) Instead, you specify the singleTop launch mode. This means that instead of starting a new activity, the search intent you receive will go to your already running PhotoGalleryActivity on top of the back stack. You receive that new intent by overriding onNewIntent(Intent) inside an Activity. Whenever you receive a new intent, you will want to refresh the items in PhotoGalleryFragment. Refactor PhotoGalleryFragment to include an updateItems() method that runs FetchItemsTask to refresh your current items. Listing 28.8 Add update method (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); new FetchItemsTask().execute(); updateItems(); } mThumbnailThread = new ThumbnailDownloader (new Handler()); mThumbnailThread.setListener(new ThumbnailDownloader.Listener () { ... }); mThumbnailThread.start(); mThumbnailThread.getLooper(); public void updateItems() { new FetchItemsTask().execute(); } Then add your onNewIntent(Intent) override in PhotoGalleryActivity to receive the new intent and refresh your PhotoGalleryFragment’s items: Listing 28.9 Override onNewIntent(…) (PhotoGalleryActivity.java) public class PhotoGalleryActivity extends SingleFragmentActivity { private static final String TAG = "PhotoGalleryActivity"; @Override public Fragment createFragment() { return new PhotoGalleryFragment(); } 460 Simple persistence with shared preferences @Override public void onNewIntent(Intent intent) { PhotoGalleryFragment fragment = (PhotoGalleryFragment) getSupportFragmentManager().findFragmentById(R.id.fragmentContainer); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { String query = intent.getStringExtra(SearchManager.QUERY); Log.i(TAG, "Received a new search query: " + query); } } } fragment.updateItems(); You should be able to see PhotoGalleryActivity receiving the new intent now in LogCat when you run a search. You should also see the items in PhotoGallery revert to Brian faces and then refresh. One important thing to note about onNewIntent(Intent): if you need the new intent value, make sure to save it someplace. The value you get from getIntent() will have the old intent, not the new one. This is because getIntent() is intended to return the intent that started this activity, not the most recent intent it received. Next up is to integrate that search query into your app. You will be implementing search so that there is only a single search query at any given time. It would be nice if that query were persistent... Simple persistence with shared preferences You could persist this data by serializing objects to flash storage like you did in Chapter 17. For simple values, though, shared preferences are simpler to implement and better behaved. Shared preferences are files on your file system that you read and edit using the SharedPreferences class. An instance of SharedPreferences acts like a key-value store, much like Bundle, except that it is backed by persistent storage. The keys are strings, and the values are atomic data types. If you look at them you will see that the files are simple XML, but SharedPreferences makes it easy to ignore that implementation detail. To start using a simple string value in shared preferences, just add a constant to use as the key for your preference value. Add your constant to FlickrFetchr: Listing 28.10 Shared preferences constant (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "FlickrFetchr"; public static final String PREF_SEARCH_QUERY = "searchQuery"; private static final String ENDPOINT = "http://api.flickr.com/services/rest/"; ... To get a specific instance of SharedPreferences, you can use the Context.getSharedPreferences(String,int) method. However, in practice, you will often not care too much about the specific instance, just that it is shared across the entire app. In that case, it is better to use the PreferenceManager.getDefaultSharedPreferences(Context) method, which returns an instance with a default name and private permissions. 461 Chapter 28 Search Acquire the default SharedPreferences and write your query out in PhotoGalleryActivity. Listing 28.11 Save out search query (PhotoGalleryActivity.java) @Override public void onNewIntent(Intent intent) { PhotoGalleryFragment fragment = (PhotoGalleryFragment)getSupportFragmentManager() .findFragmentById(R.id.fragmentContainer); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { String query = intent.getStringExtra(SearchManager.QUERY); Log.i(TAG, "Received a new search query: " + query); PreferenceManager.getDefaultSharedPreferences(this) .edit() .putString(FlickrFetchr.PREF_SEARCH_QUERY, query) .commit(); } } fragment.updateItems(); In your code above, you call SharedPreferences.edit() to get an instance of SharedPreferences.Editor. This is the class you use to stash values in your SharedPreferences. It allows you to group sets of changes together in transactions, much like you do with FragmentTransaction. If you have a lot of changes, this will allow you to group them together into a single storage write operation. Once you are done making all of your changes, you call commit() on your editor to make them visible to other users of that SharedPreferences file. Getting a value you previously stored is as simple as calling SharedPreferences.getString(…), getInt(…), or whichever method is appropriate for your data type. Add code to PhotoGalleryFragment to fetch your search query from the default SharedPreferences. Listing 28.12 Grab search query preference (PhotoGalleryFragment.java) private class FetchItemsTask extends AsyncTask > { @Override protected ArrayList doInBackground(Void... params) { String query = "android"; // just for testing Activity activity = getActivity(); if (activity == null) return new ArrayList (); } } 462 String query = PreferenceManager.getDefaultSharedPreferences(activity) .getString(FlickrFetchr.PREF_SEARCH_QUERY, null); if (query != null) { return new FlickrFetchr().search(query); } else { return new FlickrFetchr().fetchItems(); } @Override protected void onPostExecute(ArrayList items) { ... } Using SearchView on Post-Android 3.0 Preferences is your entire persistence engine for PhotoGallery. (Much easier than serializing JSON, right?) Search should now work. Run PhotoGallery, try searching for something, and see what you get. To implement canceling a search, clear out your search term from your shared preferences and call updateItems() again: Listing 28.13 Implement cancel (PhotoGalleryFragment.java) @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { ... case R.id.menu_item_clear: PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit() .putString(FlickrFetchr.PREF_SEARCH_QUERY, null) .commit(); updateItems(); return true; default: return super.onOptionsItemSelected(item); } } Using SearchView on Post-Android 3.0 Now you have a search interface that works everywhere. However, it does not work in a Honeycombapproved fashion. Honeycomb added a new class called SearchView. SearchView is an action view – a view that may be included within the action bar. SearchView allows your entire search interface to take place within your activity’s action bar, instead of inside a dialog superimposed on your activity. That means that it has the same styling and theming as your application, which is a good and fine thing. Using an action view is as simple as adding an android:actionViewClass attribute to your menu item tag, like so: Listing 28.14 Add an action view to your menu (res/menu/ fragment_photo_gallery.xml) When you specify an action view, you are saying “Hey Android, instead of using the regular view guts for this item in the action bar, use this view class instead.” Usually this also means that you get 463 Chapter 28 Search different behavior. Case in point: SearchView does not generate any onOptionsItemSelected(…) callbacks. This is a good thing because it means you can leave those callbacks in place for older devices that do not support action views. (Speaking of older devices, you may have spotted SearchViewCompat in the support library. Unfortunately, this is not what it appears to be – SearchViewCompat is not a version of SearchView that you can use on older devices. Instead, it contains a couple of static methods that make it easier to electively insert SearchView where it is available. It does not solve your problem here.) If you want, you can fire up PhotoGallery and see what SearchView looks like. It will not do anything, though. SearchView needs to know what your search configuration is before it will send you any search intents. You need to add some code to onCreateOptionsMenu(…) that pulls out your search configuration and sends it to SearchView. With the help of the SearchManager system service, this is not as hard as it sounds. SearchManager has a method called getSearchableInfo(ComponentName) that will root around in your manifest, package up the relevant information, and return it as a SearchableInfo object. Then you just have to forward your SearchableInfo along to your SearchView instance. Do this by writing the code in Listing 28.15. Listing 28.15 Configure SearchView (PhotoGalleryFragment.java) @Override @TargetApi(11) public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_photo_gallery, menu); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { // Pull out the SearchView MenuItem searchItem = menu.findItem(R.id.menu_item_search); SearchView searchView = (SearchView)searchItem.getActionView(); // Get the data from our searchable.xml as a SearchableInfo SearchManager searchManager = (SearchManager)getActivity() .getSystemService(Context.SEARCH_SERVICE); ComponentName name = getActivity().getComponentName(); SearchableInfo searchInfo = searchManager.getSearchableInfo(name); } } searchView.setSearchableInfo(searchInfo); Your first step here is simply to find your SearchView. You can do that by finding the search MenuItem by its ID, and then getting its action view by calling getActionView(). Next, you ask SearchManager for your search configuration. SearchManager is a system service that is responsible for all things search related. Earlier, it was SearchManager acting behind the scenes to pull out your search configuration and display the search interface. All the information about a search, including the name of the activity that should get the intent and everything in your searchable.xml, is stashed in the SearchableInfo object you get by calling getSearchableInfo(ComponentName) here. Once you have your SearchableInfo, you tell SearchView about it by calling setSearchableInfo(SearchableInfo). And now your SearchView is totally wired. Run and search on a post-3.0 device to see it work. 464 Challenges Figure 28.5 The search within your own activity Once SearchView is properly configured, it behaves exactly like your earlier searches did. Except for one minor detail: if you try this using the hardware keyboard on an emulator, you will see the search executed two times, one after the other. It turns out that there is a small bug in SearchView. If you hit the enter key on a hardware keyboard, it will trigger the search intent twice. In activities with a default launch mode, this would result in two identical activities starting for one search. You were preparing for this bug earlier when you set the launch mode to singleTop. You insured that intents would be sent first to an existing activity, so a new activity is not started when the duplicate search intent is sent. The duplicate intent still, annoyingly, causes the search to run twice in a row, but that is far better than having two identical activities start up for one search. Challenges Your challenges in this chapter are not too difficult. The first one is to use the Activity.startSearch(…) method. Under the hood, onSearchRequested() calls through to Activity.startSearch(…), a more detailed way of starting the search dialog. With startSearch(…), you can specify the initial query shown in the EditText where the user enters a search, add a bonus Bundle of data to send to the recipient searchable activity in the intent extras, or request a global web search dialog like you would see if you pressed the search button on the home screen. For your first small challenge, use Activity.startSearch(…) to fill in the search dialog with the current search query and highlight it. 465 Chapter 28 Search The second challenge is to display the total number of search results available in a Toast when a new search is invoked. To do this, you will need to look at the XML you are getting from Flickr. There will be a top level element attribute that tells you how many search results were returned. 466 29 Background Services All the code you have written so far has been hooked up to an activity, which means that it is associated with some screen for the user to look at. What if you do not need a screen, though? What if you need to do something out of sight and out of mind, like play music or check for new blog posts on an RSS feed? For this, you need a service. In this chapter, you will add a new feature to PhotoGallery that will allow users to poll for new search results in the background. Whenever a new search result is available, the user will receive a notification in the status bar. Creating an IntentService Let’s start by creating your service. In this chapter, you will use an IntentService. IntentService is not the only kind of service there is, but it is probably the most common. Create a subclass of IntentService called PollService. This will be the service you use to poll for search results. PollService’s onHandleIntent(Intent) method will be automatically stubbed out for you. Fill onHandleIntent(Intent) out with a log statement, add a log tag, and define a default constructor. Listing 29.1 Create PollService (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; public PollService() { super(TAG); } } @Override protected void onHandleIntent(Intent intent) { Log.i(TAG, "Received an intent: " + intent); } This is a very basic IntentService. What does it do? Well, it is sort of like an activity. It is a context (Service is a subclass of Context), and it responds to intents (as you can see in onHandleIntent(Intent)). A service’s intents are called commands. Each command is an instruction to the service to do something. Depending on the kind of service, that command could be serviced in a variety of ways. 467 Chapter 29 Background Services Figure 29.1 How IntentService services commands An IntentService services commands off of a queue. When it receives its first command, the IntentService starts up, fires up a background thread, and puts the command on a queue. The IntentService then proceeds to service each command in sequence, calling onHandleIntent(Intent) on its background thread for each command. New commands that come in go to the back of the queue. When there are no commands left in the queue, the service stops and is destroyed. This description only applies to IntentService. Later in the chapter, we will discuss the broader world of services and how commands work. You might infer from what you just learned about how IntentService works that services respond to intents. That is true! And since services, like activities, respond to intents, they must also be declared in your AndroidManifest.xml. Add an element for PollService to your manifest. 468 Creating an IntentService Listing 29.2 Add service to manifest (AndroidManifest.xml) ... Then add code to start your service inside PhotoGalleryFragment. Listing 29.3 Add service startup code (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); updateItems(); Intent i = new Intent(getActivity(), PollService.class); getActivity().startService(i); } mThumbnailThread = new ThumbnailDownloader... (new Handler()); ... Fire this up and see what you get. You should see something like this: Figure 29.2 Your service’s first steps 469 Chapter 29 Background Services What Services Are For Okay, we admit it: looking at those LogCat statements was boring. But this code is really exciting! Why? What can you do with it? Time to go back the land of make believe, where we are no longer programmers, where we work in retail shoe sales with superheroes who do our bidding. Your workers can work in two kinds of places in a store: the front of the store, where workers talk to customers, and the back of the store, where they do not. The back of the store may be larger or smaller, depending on the store. So far, all of your code has run in activities. Activities are your Android app’s storefront. All this code is focused on a pleasant visual experience for your user, your customer. Services are the back end of your Android app. Things can happen there that the user never needs to know about. Work can go on there long after the storefront has closed, when your activities are long gone. Okay, enough about stores. What can you do with a service that you cannot do with an activity? Well, for one, you can run a service while the user is occupied elsewhere. Safe background networking Your service is going to poll Flickr in the background. To perform networking in the background safely, some additional code is required. Android provides the ability for a user to turn off networking for backgrounded applications. If you have a lot of power hungry applications, this can be a big performance improvement. This does mean, however, that if you are doing networking in the background, you need to verify with the ConnectivityManager that the network is available. Because the API has changed around historically, this requires two checks, not one. The first check is to verify that ConnectivityManager.getBackgroundDataSetting() is true, and the second is to verify that ConnectivityManager.getActiveNetworkInfo() is not null. Add the code in Listing 29.4 to perform these checks, and we will get into the gory details once you are done. Listing 29.4 Check for background network availability (PollService.java) @Override public void onHandleIntent(Intent intent) { ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); @SuppressWarnings("deprecation") boolean isNetworkAvailable = cm.getBackgroundDataSetting() && cm.getActiveNetworkInfo() != null; if (!isNetworkAvailable) return; } Log.i(TAG, "Received an intent: " + intent); Now for why you need two checks. In older versions of Android, you were supposed to check getBackgroundDataSetting() and bail out if it returned false. If you did not check it, then you could 470 Looking for New Results use background data all you wanted. This did not work very well. It was just too easy to ignore the check entirely, or inadvertently forget it. In Android 4.0, Ice Cream Sandwich, this was changed so that the background data setting simply disabled the network entirely. Which is why you check to see whether getActiveNetworkInfo() returns null. If it is null, then the networking does not work at all. This is good for the user, because it means that the background data settings always do what they expect. Of course it is a little more work for you, the developer. To use getActiveNetworkInfo(), you also need to acquire the ACCESS_NETWORK_STATE permission. Listing 29.5 Acquire network state permission (AndroidManifest.xml) Looking for New Results Your service will be polling for new results, so it will need to know what the last result fetched was. This is a perfect job for another SharedPreferences entry. Add another constant to FlickrFetchr to store the ID of the most recently fetched photo. Listing 29.6 Add recent ID preference constant (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "PhotoFetcher"; public static final String PREF_SEARCH_QUERY = "searchQuery"; public static final String PREF_LAST_RESULT_ID = "lastResultId"; private static final String ENDPOINT = "http://api.flickr.com/services/rest/"; private static final String API_KEY = "xxx"; ... 471 Chapter 29 Background Services The next step is to fill out your service. Here is what you need to do: 1. Pull out the current query and the last result ID from the default SharedPreferences. 2. Fetch the latest result set with FlickrFetchr. 3. If there are results, grab the first one. 4. Check to see if it is different from the last result ID. 5. Store the first result back in SharedPreferences. Return to PollService.java and put this plan into action. Listing 29.7 shows a long swath of code, but it uses nothing you have not seen before. Listing 29.7 Checking for new results (PollService.java) @Override protected void onHandleIntent(Intent intent) { ... if (!isNetworkAvailable) return; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String query = prefs.getString(FlickrFetchr.PREF_SEARCH_QUERY, null); String lastResultId = prefs.getString(FlickrFetchr.PREF_LAST_RESULT_ID, null); ArrayList ... items; if (query != null) { items = new FlickrFetchr().search(query); } else { items = new FlickrFetchr().fetchItems(); } if (items.size() == 0) return; String resultId = items.get(0).getId(); if (!resultId.equals(lastResultId)) { Log.i(TAG, "Got a new result: " + resultId); } else { Log.i(TAG, "Got an old result: " + resultId); } } prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); See each part we discussed above? Good. Run PhotoGallery, and you should see your app getting new results initially. If you have a search query selected, you will probably see stale results when you subsequently start up the app. Delayed Execution with AlarmManager To actually use your service in the background, you will need some way to make things happen when none of your activities are running. Say, by making a timer that goes off every five minutes or so. 472 Delayed Execution with AlarmManager You could do this with a Handler by calling Handler.sendMessageDelayed(…) or Handler.postDelayed(…). This solution will probably fail if the user navigates away from all your activities, though. The process will shut down, and your Handler messages will go kaput with it. So instead of Handler, you will use AlarmManager. AlarmManager is a system service that can send Intents for you. How do you tell AlarmManager what intents to send? You use a PendingIntent. You can use PendingIntent to package up a wish: “I want to start PollService.” You can then send that wish to other components on the system, like AlarmManager. Write a new method called setServiceAlarm(Context,boolean) inside PollService that turns an alarm on and off for you. You will write it as a static method. That keeps your alarm code with the other code in PollService that it is related to, but allows other components to invoke it. You will usually want to turn it on and off from front end code in a fragment or other controller. Listing 29.8 Add alarm method (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; private static final int POLL_INTERVAL = 1000 * 15; // 15 seconds public PollService() { super(TAG); } @Override public void onHandleIntent(Intent intent) { ... } public static void setServiceAlarm(Context context, boolean isOn) { Intent i = new Intent(context, PollService.class); PendingIntent pi = PendingIntent.getService( context, 0, i, 0); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); } } if (isOn) { alarmManager.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), POLL_INTERVAL, pi); } else { alarmManager.cancel(pi); pi.cancel(); } The first thing you do in your method is construct your PendingIntent that starts PollService. You do this by calling PendingIntent.getService(…). PendingIntent.getService(…) packages up an invocation of Context.startService(Intent). It takes in four parameters: a Context with which to send the intent, a request code that you can use to distinguish this PendingIntent from others, the Intent object to send, and finally a set of flags that you can use to tweak how the PendingIntent is created. (You will use one of these in a moment). 473 Chapter 29 Background Services After that, you need to either set the alarm or cancel it. To set the alarm, you call AlarmManager.setRepeating(…). This method also takes four parameters: a constant to describe the time basis for the alarm (about which more in a moment), the time at which to start the alarm, the time interval at which to repeat the alarm, and finally a PendingIntent to fire when the alarm goes off. Canceling the alarm is done by calling AlarmManager.cancel(PendingIntent). You will also usually want to cancel the PendingIntent, too. In a moment, you will see how canceling the PendingIntent also helps us track the status of the alarm. Add some quick test code to run it from within PhotoGalleryFragment. Listing 29.9 Add alarm startup code (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); updateItems(); Intent i = new Intent(getActivity(), PollService.class); getActivity().startService(i); PollService.setServiceAlarm(getActivity(), true); } mThumbnailThread = new ThumbnailDownloader (new Handler()); ... Finish typing in this code and run PhotoGallery. Then immediately hit the back button and exit out of the app. Notice anything in LogCat? PollService is faithfully chugging along, running again every 15 seconds. This is what AlarmManager is designed to do. Even if your process gets shut down, AlarmManager will keep on firing intents to start PollService again and again. (This behavior is, of course, extremely annoying. You may want to uninstall the app until we get it all straightened out.) PendingIntent Let’s talk a little bit more about PendingIntent. A PendingIntent is a token object. When you get one here by calling PendingIntent.getService(…), you say to the OS, “Please remember that I want to send this intent with startService(Intent).” Later on you can call send() on your PendingIntent token, and the OS will send the intent you originally wrapped up in exactly the way you asked. The really nice thing about this is that when you give that PendingIntent token to someone else and they use it, it sends that token as your application. Also, since the PendingIntent itself lives in the OS, not in the token, you maintain control of it. If you wanted to be cruel, you could give someone else a PendingIntent object and then immediately cancel it, so that send() does nothing. If you request a PendingIntent twice with the same intent, you will get the same PendingIntent. You can use this to test whether a PendingIntent already exists or to cancel a previously issued PendingIntent. 474 Managing alarms with PendingIntent Managing alarms with PendingIntent You can only register one alarm for each PendingIntent. That is how setServiceAlarm(Context,boolean) works when isOn is false: it calls AlarmManager.cancel(PendingIntent) to cancel the alarm for your PendingIntent, and then cancels your PendingIntent. Since the PendingIntent is also cleaned up when the alarm is canceled, you can check whether that PendingIntent exists or not to see whether the alarm is active or not. This is done by passing in the PendingIntent.FLAG_NO_CREATE flag to PendingIntent.getService(…). This flag says that if the PendingIntent does not already exist, return null instead of creating it. Write a new method called isServiceAlarmOn(Context) that uses PendingIntent.FLAG_NO_CREATE to tell whether the alarm is on or not. Listing 29.10 Add isServiceAlarmOn() method (PollService.java) public static void setServiceAlarm(Context context, boolean isOn) { ... } public static boolean isServiceAlarmOn(Context context) { Intent i = new Intent(context, PollService.class); PendingIntent pi = PendingIntent.getService( context, 0, i, PendingIntent.FLAG_NO_CREATE); return pi != null; } Since this PendingIntent is only used for setting your alarm, a null PendingIntent here means that your alarm is not set. Controlling Your Alarm Now that you can turn your alarm on and off (as well as tell whether it is on or off), let’s add an interface to turn this thing on and off. Add another menu item to menu/fragment_photo_gallery.xml. Listing 29.11 Add service toggle (menu/fragment_photo_gallery.xml) 475 Chapter 29 Background Services Then you need to add a few new strings – one to start polling, and one to stop polling. (You will need a couple of other ones later, too, for a status bar notification. Go ahead and add those as well.) Listing 29.12 Add polling strings (res/values/strings.xml) ... Now delete your old debug code for starting the alarm, and add an implementation for the menu item. Listing 29.13 Toggle menu item implementation (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); updateItems(); PollService.setServiceAlarm(getActivity(), true); } mThumbnailThread = new ThumbnailDownloaderSearch name="clear_search">Clear Search name="start_polling">Poll for new pictures name="stop_polling">Stop polling name="new_pictures_title">New PhotoGallery Pictures name="new_pictures_text">You have new pictures in PhotoGallery.(new Handler()); ... ... @Override @TargetApi(11) public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_search: ... case R.id.menu_item_clear: ... updateItems(); return true; case R.id.menu_item_toggle_polling: boolean shouldStartAlarm = !PollService.isServiceAlarmOn(getActivity()); PollService.setServiceAlarm(getActivity(), shouldStartAlarm); } 476 } return true; default: return super.onOptionsItemSelected(item); Updating options menu items With that, you should be able to toggle your alarm on and off. How to update your menu item, though? Well! Updating options menu items Usually, all you have to do is inflate an options menu. Sometimes, though, you need to update the options menu to reflect the state of your application. Now, options menus are not inflated every time they are used, even on old style options menus. If you need to update the contents of an options menu item, you should instead put that code in onPrepareOptionsMenu(Menu). This method is called every single time the menu needs to be configured, not just when it is created the first time. Add an implementation of onPrepareOptionsMenu(Menu) that checks to see whether the alarm is on, and then changes the text of menu_item_toggle_polling to show the appropriate feedback to the user. Listing 29.14 Add onPrepareOptionsMenu(Menu) (PhotoGalleryFragment.java) @Override public boolean onOptionsItemSelected(MenuItem item) { ... } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); } MenuItem toggleItem = menu.findItem(R.id.menu_item_toggle_polling); if (PollService.isServiceAlarmOn(getActivity())) { toggleItem.setTitle(R.string.stop_polling); } else { toggleItem.setTitle(R.string.start_polling); } In pre-3.0 devices, this method is called every time the menu is displayed, which ensures that your menu item always shows the right text. Run PhotoGallery on a pre-3.0 emulator if you would like to check this yourself. After 3.0, though, this is not enough. The action bar does not automatically update itself. You have to manually tell it to call onPrepareOptionsMenu(Menu) and refresh its items by calling Activity.invalidateOptionsMenu(). Add the following code to onOptionsItemSelected(MenuItem) to tell post-3.0 devices to update their action bars: 477 Chapter 29 Background Services Listing 29.15 Invalidate your options menu (PhotoGalleryFragment.java) @Override @TargetApi(11) public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { ... case R.id.menu_item_toggle_polling: boolean shouldStartAlarm = !PollService.isServiceAlarmOn(getActivity()); PollService.setServiceAlarm(getActivity(), shouldStartAlarm); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) getActivity().invalidateOptionsMenu(); } } return true; default: return super.onOptionsItemSelected(item); With that, your code should work great on a new 4.2 emulator, too. And yet. There is something missing. Notifications Your service is now running and doing its thing in the background. The user never knows a thing about it, though, so it is not worth much. When your service needs to communicate something to the user, the proper tool is almost always a notification. Notifications are items that appear in the notifications drawer, which the user can access by dragging it down from the top of the screen. To post a notification, you first need to create a Notification object. Notifications are created by using a builder object, much like AlertDialog was in Chapter 12. At a minimum, your Notification should have: • ticker text to display in the status bar when the notification is first shown • an icon to show in the status bar after the ticker text goes away • a view to show in the notification drawer to represent the notification itself • a PendingIntent to fire when the user presses the notification in the drawer Once you have created a Notification object, you can post it by calling notify(int, on the NotificationManager system service. Notification) Make PollService notify the user that a new result is ready by adding the code in Listing 29.16, which creates a Notification and calls NotificationManager.notify(int, Notification). 478 Notifications Listing 29.16 Add a notification (PollService.java) @Override public void onHandleIntent(Intent intent) { ... String resultId = items.get(0).getId(); if (!resultId.equals(lastResultId)) { Log.i(TAG, "Got a new result: " + resultId); Resources r = getResources(); PendingIntent pi = PendingIntent .getActivity(this, 0, new Intent(this, PhotoGalleryActivity.class), 0); Notification notification = new NotificationCompat.Builder(this) .setTicker(r.getString(R.string.new_pictures_title)) .setSmallIcon(android.R.drawable.ic_menu_report_image) .setContentTitle(r.getString(R.string.new_pictures_title)) .setContentText(r.getString(R.string.new_pictures_text)) .setContentIntent(pi) .setAutoCancel(true) .build(); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } } notificationManager.notify(0, notification); prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); Let’s go over this from top to bottom. First, you configure the ticker text and small icon by calling setTicker(CharSequence) and setSmallIcon(int). After that, you configure the appearance of your Notification in the drawer itself. It is possible to create a completely custom look and feel, but it is easier to use the standard look for a notification, which features an icon, a title, and a text area. It will use the value from setSmallIcon(int) for the icon. To set the title and text, you call setContentTitle(CharSequence) and setContentText(CharSequence), respectively. Next, you must specify what happens when the user presses your Notification. Like AlarmManager, this is done using a PendingIntent. The PendingIntent you pass in to setContentIntent(PendingIntent) will be fired when the user presses your Notification in the drawer. Calling setAutoCancel(true) tweaks that behavior a little bit. With setAutoCancel(true) set, your notification will also be deleted from the notification drawer when the user presses it. Finally, there is the call to NotificationManager.notify(…). The integer parameter you pass in is an identifier for your notification. It should be unique across your application. If you post a second notification with this same ID, it will replace the last notification you posted with that ID. This is how you would implement a progress bar or other dynamic visuals. And that is it. Your entire backgrounded user interface! After you are satisfied that everything is working correctly, change your alarm constant to be something more sensible: 479 Chapter 29 Background Services Listing 29.17 Change to a sensible alarm constant (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; public static final int POLL_INTERVAL = 1000 * 15; // 15 seconds public static final int POLL_INTERVAL = 1000 * 60 * 5; // 5 minutes public PollService() { super(TAG); } For the More Curious: Service Details We recommend using IntentService for most service tasks. If the IntentService pattern does not suit your architecture, though, you will need to understand more about services to implement your own. Prepare for an infobomb, though – there are a lot of details and ins and outs to using services. What service does (and does not) do A service is an application component that provides lifecycle callbacks, just like an activity. Those callbacks are even performed on the main UI thread for you, just like in an activity. A service does not run any code on a background thread out of the box. This is the #1 reason we recommend IntentService. Most non-trivial services will require a background thread of some kind, and IntentService automatically manages the boilerplate code you need to accomplish that. Let’s look and see what lifecycle callbacks a service has. A service’s lifecycle For a service started with startService(Intent), life is fairly simple. There are four lifecycle callbacks. • onCreate(…) – called when the service is created. • onStartCommand(Intent,int,int) – called once each time a component starts the service with startService(Intent). The two integer parameters are a set of flags and a start ID. The flags are used to signify whether this intent delivery is an attempt to redeliver an intent, or if it is an attempt to retry a delivery which never made it to or returned from onStartCommand(Intent,int,int). The start ID will be different for every call to onStartCommand(Intent,int,int), so it may be used to distinguish this command from others. • – called when the service no longer needs to be alive. Often this will be after the service is stopped. onDestroy() One question remains: how does the service stop? This can happen in different ways, depending on what type of service you have written. The type of service is determined by the value returned from onStartCommand(…), which may be Service.START_NOT_STICKY, START_REDELIVER_INTENT, or START_STICKY. 480 Non-sticky services Non-sticky services is a non-sticky service, so let’s start there. A non-sticky service stops when the service itself says it is done. To make your service non-sticky, return either START_NOT_STICKY or START_REDELIVER_INTENT. IntentService You tell Android that you are done by calling either stopSelf() or stopSelf(int). The first method, stopSelf(), is unconditional. It will always stop your service, no matter how many times onStartCommand(…) has been called. IntentService uses stopSelf(int) instead. This method takes in the start ID received in onStartCommand(…). This method will only stop your service if this was the most recent start received. (This is how IntentService works under the hood.) ID So what is the difference between returning START_NOT_STICKY and START_REDELIVER_INTENT? The difference is in how your service behaves if the system needs to shut it down before it is done. A START_NOT_STICKY service will die and disappear into the void. START_REDELIVER_INTENT, on the other hand, will attempt to start up the service again later, when resources are less constrained. Choosing between START_NOT_STICKY and START_REDELIVER_INTENT is a matter of deciding how important that operation is to your application. If the service is not critical, choose START_NOT_STICKY. In PhotoGallery, your service is being run repeatedly on an alarm. If one invocation falls through the cracks, it is not a big deal, so: START_NOT_STICKY. This is the default behavior for IntentService. To switch to using START_REDELIVER_INTENT, call IntentService.setIntentRedelivery(true). Sticky services A sticky service stays started until something outside the service tells it to stop by calling Context.stopService(Intent). To make your service sticky, return START_STICKY. Once a sticky service is started it is “on” until a component calls Context.stopService(Intent). If the service needs to be killed for some reason, it will be restarted again with a null intent passed in to onStartCommand(…). A sticky service may be appropriate for a long-running service, like a music player, which needs to stick around until the user tells it to stop. Even then, it is worth considering an alternative architecture using non-sticky services. Sticky service management is inconvenient, because it is difficult to tell whether the service is already started. Bound services In addition to all this, it is possible to bind to a service by using the bindService(Intent,ServiceConnection,int) method. Service binding is a way to connect to a service and call methods on it directly. You bind to a service by calling bindService(Intent,ServiceConnection,int). ServiceConnection is an object that represents your service binding and receives all binding callbacks. 481 Chapter 29 Background Services In a fragment, your binding code would look something like this: private ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // Used to communicate with the service MyBinder binder = (MyBinder)service; } public void onServiceDisconnected(ComponentName className) { } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent i = new Intent(c, MyService.class); c.bindService(i, mServiceConnection, 0); } @Override public void onDestroy() { super.onDestroy(); getActivity().getApplicationContext().unbindService(mServiceConnection); } On the service’s side, binding introduces two additional lifecycle callbacks: • onBind(Intent) – called every time the service is bound to. Returns the IBinder in ServiceConnection.onServiceConnected(ComponentName,IBinder). • onUnbind(Intent) object received – called when a service’s binding is terminated. Local service binding So what does MyBinder look like? If the service is a local service, then it may be a simple Java object that lives in your local process. Usually this is used to provide a handle to directly call methods on your service: private class MyBinder extends IBinder { public MyService getService() { return MyService.this; } } @Override public void onBind(Intent intent) { return new MyBinder(); } This pattern looks exciting. It is the only place in Android that enables one Android component to directly talk to another. We do not recommend it, though. Since services are effectively singletons, using them this way provides no major benefits over just using a singleton instead. 482 Bound services Remote service binding Binding is more useful for remote services, because they give applications in other processes the ability to invoke methods on your service. Creating a remote service binder is an advanced topic and beyond the scope of this book. Check out AIDL or the Messenger class for more details. 483 This page intentionally left blank 30 Broadcast Intents Things are happening all the time on an Android device. WiFi is going in and out of range, packages are getting installed, phone calls and text messages are coming in. When many components on the system need to know that some event has occurred, Android uses a broadcast intent to tell everyone about it. Broadcast intents work similarly to the intents you already know and love, except that they can be received by multiple components at the same time. Broadcast intents are received by broadcast receivers. Figure 30.1 Regular intents vs. broadcast intents In this chapter, you will learn how to listen to broadcast intents from the system, as well as how to dynamically send and receive them within your app at runtime. You will start by listening for a broadcast that informs you that the device has booted up, and then later you will send and receive your own broadcast intent. Waking Up on Boot PhotoGallery’s background alarm works, but it is not perfect. If the user reboots their phone, the alarm will be forgotten. Apps that perform an ongoing process for the user usually need to wake themselves up after the device is booted. You can detect when boot is completed by listening for a broadcast intent with the BOOT_COMPLETED action. 485 Chapter 30 Broadcast Intents Broadcast receivers in the manifest Time to write a broadcast receiver. Start by creating a new Java class called StartupReceiver that is a subclass of android.content.BroadcastReceiver. Eclipse will implement one abstract method for you called onReceive(Context,Intent). Fill out your class like so: Listing 30.1 Your first broadcast receiver (StartupReceiver.java) package com.bignerdranch.android.photogallery; ... public class StartupReceiver extends BroadcastReceiver { private static final String TAG = "StartupReceiver"; @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "Received broadcast intent: " + intent.getAction()); } } This is your broadcast receiver. A broadcast receiver is a component that receives intents, just like a service or an activity. And just like services and activities, broadcast receivers must be registered with the system in order to do anything useful. That does not always mean that they need to be in your manifest. This particular receiver does, though. Hooking up a receiver works just like hooking up a service or activity. You use the receiver tag with appropriate intent-filters within. StartupReceiver will be listening for the BOOT_COMPLETED action. This action also requires a permission, so you will need to include an appropriate usespermission tag as well. Open AndroidManifest.xml and hook up StartupReceiver. Listing 30.2 Adding your receiver to the manifest (AndroidManifest.xml) Unlike activities and services, broadcast receivers declared in the manifest almost always declare intent filters. The entire reason broadcast intents exist is to send information to more than one listener, but explicit intents only have one receiver. As a result, the explicit broadcast intent is a rare bird. Figure 30.2 Receiving BOOT_COMPLETED With your broadcast receiver declared in your manifest, it will wake up anytime a matching broadcast intent is sent – even if your app is not currently running. Upon waking up, the ephemeral broadcast receiver’s onReceive(Context,Intent) method will be run, and then it will die. Run PhotoGallery, reboot your device or emulator and switch over to DDMS. You should see a LogCat statement showing that your receiver ran. If you check your device in the Devices tab, though, you will probably not see a process for PhotoGallery. Your process came to life just long enough to run your broadcast receiver, and then it died again. 487 Chapter 30 Broadcast Intents How to use receivers The fact that broadcast receivers live such short lives restricts the things you can do with them. You cannot use any asynchronous APIs, for example, or register any listeners, because your receiver will not be alive any longer than the length of onReceive(Context,Intent). onReceive(Context,Intent) runs on your main thread, too, so you cannot do any heavy lifting inside it. That means no networking or heavy work with permanent storage. This does not make receivers useless, though. They are invaluable for all kinds of little plumbing code. Your recurring alarm needs to be reset when the system finishes starting. This is a small enough task that a broadcast receiver is perfect for the job. Your receiver will need to know whether the alarm should be on or off. Add a preference constant to PollService to store this information. Listing 30.3 Add alarm status preference (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; private static final int POLL_INTERVAL = 1000 * 60 * 5; // 5 minutes public static final String PREF_IS_ALARM_ON = "isAlarmOn"; public PollService() { super(TAG); } ... public static void setServiceAlarm(Context context, boolean isOn) { Intent i = new Intent(context, PollService.class); PendingIntent pi = PendingIntent.getService( context, 0, i, 0); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (isOn) { alarmManager.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), POLL_INTERVAL, pi); } else { alarmManager.cancel(pi); pi.cancel(); } } PreferenceManager.getDefaultSharedPreferences(context) .edit() .putBoolean(PollService.PREF_IS_ALARM_ON, isOn) .commit(); Then your StartupReceiver can use it to turn the alarm on at boot. 488 Filtering Foreground Notifications Listing 30.4 Start alarm on boot (StartupReceiver.java) @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "Received broadcast intent: " + intent.getAction()); } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean isOn = prefs.getBoolean(PollService.PREF_IS_ALARM_ON, false); PollService.setServiceAlarm(context, isOn); Run PhotoGallery again. This time, your background polling should be restarted after you reboot your phone, tablet, or emulator. Filtering Foreground Notifications With that sharp corner filed down a bit, we turn to another imperfection in PhotoGallery. Your notifications work great, but they are sent even when the user already has the application open. You can fix this problem with broadcast intents, too. But they will work in a completely different way. Sending broadcast intents This is the easiest part of the solution: sending your own broadcast intent. To send a broadcast intent, just create an intent and pass it in to sendBroadcast(Intent). In this case, you will want it to broadcast an action you define, so define an action constant as well. Add the following bits of code to PollService. 489 Chapter 30 Broadcast Intents Listing 30.5 Sending a broadcast intent (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; private static final int POLL_INTERVAL = 1000 * 60 * 5; // 5 minutes public static final String PREF_IS_ALARM_ON = "isAlarmOn"; public static final String ACTION_SHOW_NOTIFICATION = "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION"; public PollService() { super(TAG); } @Override public void onHandleIntent(Intent intent) { ... if (!resultId.equals(lastResultId)) { ... NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, notification); } } sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION)); prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); Dynamic broadcast receivers Next up is to receive your broadcast intent. You could write a broadcast receiver like StartupReceiver registered in the manifest to handle this. But that would not do the right thing in your case. Here, you really want PhotoGalleryFragment to receive the intent only while it is alive. A standalone receiver declared in the manifest would not do that job easily. It would always receive the intent, and would need some other way of knowing that PhotoGalleryFragment is alive. The solution is to use a dynamic broadcast receiver. A dynamic receiver is registered in code, not in the manifest. You register the receiver by calling registerReceiver(BroadcastReceiver, IntentFilter) and unregister it by calling unregisterReceiver(BroadcastReceiver). The receiver itself is typically defined as an inner instance, like a button-click listener. However, since you need the same instance in registerReceiver(…) and unregisterReceiver(…), you will need to assign the receiver to an instance variable. Create a new abstract class called VisibleFragment, with Fragment as its superclass. This class will be a generic fragment that hides foreground notifications. (You will write another fragment like this in Chapter 31.) 490 Dynamic broadcast receivers Listing 30.6 A receiver of VisibleFragment’s own (VisibleFragment.java) package com.bignerdranch.android.photogallery; ... public abstract class VisibleFragment extends Fragment { public static final String TAG = "VisibleFragment"; private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Toast.makeText(getActivity(), "Got a broadcast:" + intent.getAction(), Toast.LENGTH_LONG) .show(); } }; @Override public void onResume() { super.onResume(); IntentFilter filter = new IntentFilter(PollService.ACTION_SHOW_NOTIFICATION); getActivity().registerReceiver(mOnShowNotification, filter); } } @Override public void onPause() { super.onPause(); getActivity().unregisterReceiver(mOnShowNotification); } Note that to pass in an IntentFilter, you have to create one in code. Your IntentFilter here is identical to the filter specified by the following XML: 486 Broadcast receivers in the manifest ... Any IntentFilter you can express in XML can also be expressed in code this way. Just call addCategory(String), addAction(String), addDataPath(String), and so on to configure your filter. Dynamically registered broadcast receivers must also take care to clean themselves up. Typically, if you register a receiver in a startup lifecycle method, you will call Context.unregisterReceiver(BroadcastReceiver) in the corresponding shutdown method. So here you register inside onResume(), and unregister inside onPause(). Similarly, if you registered inside onActivityCreated(…), you would unregister inside onActivityDestroyed(). (Be careful with onCreate(…) and onDestroy() in retained fragments, by the way. getActivity() will return different values in onCreate(…) and onDestroy() if the screen has rotated. If you want to register/unregister in Fragment.onCreate(Bundle) and Fragment.onDestroy(), use getActivity().getApplicationContext() instead.) Modify PhotoGalleryFragment to be a subclass of your new VisibleFragment. 491 Chapter 30 Broadcast Intents Listing 30.7 Make your fragment visible (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { public class PhotoGalleryFragment extends VisibleFragment { GridView mGridView; ArrayList mItems; ThumbnailDownloader mThumbnailThread; Run PhotoGallery and toggle background polling a couple of times. You will see a nice toast pop up in addition to your notification ticker up top. Figure 30.3 Proof that your broadcast exists Using private permissions One problem with using a broadcast like this is that anyone on the system can listen to it or trigger your receivers. You are usually not going to want either of those things to happen. You can preclude these unauthorized intrusions into your personal business in a couple of ways. If the receiver is declared in your manifest and is internal to your app, you can add an android:exported="false" attribute to your receiver tag. This will prevent it from being visible to other applications on the system. In other circumstances, you can create your own permission. This is done by adding a permission tag to your AndroidManifest.xml. Add the following XML to AndroidManifest.xml to declare and acquire your own permission. 492 Using private permissions Listing 30.8 Add a private permission (AndroidManifest.xml) In this code, you defined your own custom permission with a protection level of signature. You will learn more about protection levels in just a moment. The permission itself is a simple string, just like intent actions, categories, and system permissions you have used. You must always acquire a permission to use it, even when you defined it yourself. Them’s the rules. Take note of the shaded constant value above, by the way. This string needs to appear in three different places, and must be identical in each place. You would be wise to copy and paste it rather than typing it out by hand. Next, use your permission by defining a corresponding constant in code and then passing it in to your sendBroadcast(…) call. 493 Chapter 30 Broadcast Intents Listing 30.9 Sending with a permission (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; private static final int POLL_INTERVAL = 1000 * 60 * 5; // 5 minutes public static final String PREF_IS_ALARM_ON = "isAlarmOn"; public static final String ACTION_SHOW_NOTIFICATION = "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION"; public static final String PERM_PRIVATE = "com.bignerdranch.android.photogallery.PRIVATE"; public PollService() { super(TAG); } @Override public void onHandleIntent(Intent intent) { ... if (!resultId.equals(lastResultId)) { ... NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, notification); } } sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION)); sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE); prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); To use your permission, you pass it as a parameter to sendBroadcast(…). Using the permission here makes it so that any application must use that same permission to receive the intent you are sending. What about your broadcast receiver? Someone could create their own broadcast intent to trigger it. You can fix that by passing in your permission in registerReceiver(…), too. Listing 30.10 Permissions on a broadcast receiver (VisibleFragment.java) @Override public void onResume() { super.onResume(); IntentFilter filter = new IntentFilter(PollService.ACTION_SHOW_NOTIFICATION); getActivity().registerReceiver(mOnShowNotification, filter); getActivity().registerReceiver(mOnShowNotification, filter, PollService.PERM_PRIVATE, null); } Now, your app is the only app that can trigger that receiver. 494 Receiving results with ordered broadcasts More about protection levels Every custom permission has to specify a value for android:protectionLevel. Your permission’s protectionLevel tells Android how it should be used. In your case, you used a protectionLevel of signature. The signature protection level means that if another application wants to use your permission, it has to be signed with the same key as your application. This is usually the right choice for permissions you use internally in your application. Since other developers don’t have your key, they can’t get access to anything this permission protects. Plus, since you do have your own key, you can use this permission in any other app you decide to write later. Table 30.1 Values for protectionLevel Value Description normal This is for protecting app functionality that won’t do anything dangerous, like access secure personal data or send data to the internet. The user can see the permission before choosing to install the app, but they aren’t explicitly asked to grant it. android.permission.RECEIVE_BOOT_COMPLETED uses this permission level, and so does the permission to let you vibrate your phone. Think of this as things that aren’t dangerous, but you’d want the user to know that you might do them anyway. dangerous This is for everything we said you would not use normal for – for accessing personal data, sending and receiving things from network interfaces, for accessing hardware that might be used to spy on you, or anything that could cause real problems for the user. The internet permission, camera permission, and contacts permission all fall under this category. Android may ask the user for an explicit go ahead before approving a dangerous permission. signature The system grants this permission if the app is signed with the same certificate as the declaring application, and denies it otherwise. If the permission is granted, the user isn’t notified. This is what you would use for functionality that is internal to your own app – since you have the certificate, and only apps signed with the same certificate can use the permission, you have control over who uses the permission. Here, you have used it to lock down anyone else from seeing your broadcasts, but if you wanted to you could write another app that listens to them, too. signatureOrSystem This is like signature, but it also grants permission to all packages in the Android system image. This is used to communicate with apps built into the system image, so you probably don’t need to worry about it. Receiving results with ordered broadcasts Time to finally bring this baby home. You are sending your own personal private broadcast, but so far you only have one-way communication. 495 Chapter 30 Broadcast Intents Figure 30.4 Regular broadcast intents This is because a regular broadcast intent is conceptually received by everyone at the same time. Now, onReceive(…) is called on the main thread, so in practice your receivers are not actually executed concurrently. It is not possible to rely on their being executed in any particular order, though, or to know when they have all completed execution. As a result, it is a hassle for the broadcast receivers to communicate with each other, or for the sender of the intent to receive information from the receivers. Figure 30.5 Ordered broadcast intents You can implement two-way communication using an ordered broadcast intent. Ordered broadcasts allow a sequence of broadcast receivers to process a broadcast intent in order. They also allow the sender of a broadcast to receive results from the broadcast’s recipients by passing in a special broadcast receiver, called the result receiver. On the receiving side, this looks mostly the same as a regular broadcast. You get an additional tool, though: a set of methods used to change the return value of your receiver. Here, you want to cancel the notification. This can be communicated by use of a simple integer result code. So you use the setResultCode(int) method to set the result code to Activity.RESULT_CANCELED. Modify VisibleFragment to send some information back to the sender of SHOW_NOTIFICATION. 496 Receiving results with ordered broadcasts Listing 30.11 Send a simple result back (VisibleFragment.java) private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Toast.makeText(getActivity(), "Got a broadcast:" + intent.getAction(), Toast.LENGTH_LONG) .show(); // If we receive this, we're visible, so cancel // the notification Log.i(TAG, "canceling notification"); setResultCode(Activity.RESULT_CANCELED); } }; Since all you need to do is signal yes or no here, you only need the result code. If you need to return more complicated data, you can use setResultData(String) or setResultExtras(Bundle). And if you want to set all three values, you can call setResult(int,String,Bundle). Once your return values are set here, every subsequent receiver will be able to see or modify them. For those methods to do anything useful, your broadcast needs to be ordered. Write a new method to send an ordered broadcast in PollService. This method will package up a Notification invocation and send it out as a broadcast. A result receiver will be specified to send out the packaged Notification (provided it has not been canceled). Listing 30.12 Send an ordered broadcast (PollService.java) void showBackgroundNotification(int requestCode, Notification notification) { Intent i = new Intent(ACTION_SHOW_NOTIFICATION); i.putExtra("REQUEST_CODE", requestCode); i.putExtra("NOTIFICATION", notification); } sendOrderedBroadcast(i, PERM_PRIVATE, null, null, Activity.RESULT_OK, null, null); Context.sendOrderedBroadcast(Intent,String,BroadcastReceiver,Handler,int,String,Bundle) has five additional parameters beyond the ones you used in sendBroadcast(Intent,String). They are, in order: a result receiver, a Handler to run the result receiver on, and then initial values for the result code, result data, and result extras for the ordered broadcast. The result receiver is a special receiver that will run after all the other recipients of your ordered broadcast intent. In other circumstances, you would be able to use the result receiver here to receive the broadcast and post the notification object. Here, though, that will not work. This broadcast intent will often be sent right before PollService dies. That means that your broadcast receiver might be dead, too. Thus, your final broadcast receiver will need to be standalone. Create a new BroadcastReceiver subclass called NotificationReceiver. Implement it as follows: 497 Chapter 30 Broadcast Intents Listing 30.13 Implement your result receiver (NotificationReceiver.java) public class NotificationReceiver extends BroadcastReceiver { private static final String TAG = "NotificationReceiver"; @Override public void onReceive(Context c, Intent i) { Log.i(TAG, "received result: " + getResultCode()); if (getResultCode() != Activity.RESULT_OK) // A foreground activity cancelled the broadcast return; int requestCode = i.getIntExtra("REQUEST_CODE", 0); Notification notification = (Notification) i.getParcelableExtra("NOTIFICATION"); } } NotificationManager notificationManager = (NotificationManager) c.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(requestCode, notification); Finally, register your new receiver. Since it sends the notification, receiving the result set by the other receivers, it should run after everything else. That means that you will need to set a low priority for your receiver. Since your receiver should run last, give it a priority of -999 (-1000 and below are reserved). And since this receiver is only used internally by your application, you do not need it to be externally visible. Set android:exported="false" to keep this receiver to yourself. Listing 30.14 Register notification receiver (AndroidManifest.xml) android:name="android.permission.ACCESS_NETWORK_STATE" /> android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> android:name="com.bignerdranch.android.photogallery.PRIVATE" /> ... ... Now use your new method instead of NotificationManager to post your notification. 498 Receivers and Long-running Tasks Listing 30.15 Finish it up (PollService.java) @Override public void onHandleIntent(Intent intent) { ... if (!resultId.equals(lastResultId)) { ... Notification notification = new NotificationCompat.Builder(this) ... .build(); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, notification); sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE); } } showBackgroundNotification(0, notification); prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); Run PhotoGallery and toggle background polling a couple of times. You should see that notifications no longer appear. To verify that notifications still work in the background, set PollService.POLL_INTERVAL to five seconds again so that you do not have to wait five whole minutes. Receivers and Long-running Tasks So what do you do if you want a broadcast intent to kick off a longer-running task than the restrictions of the main run loop allow? You have two options. The first is to put that work into a service instead, and start the service in your broadcast receiver’s small window of opportunity. This is the method we recommend. A service can take as long as it needs to take to service a request. It can queue up multiple requests and service them in order, or otherwise manage requests as it sees fit. The second is to use the BroadcastReceiver.goAsync() method. This method returns a BroadcastReceiver.PendingResult object, which can be used to provide a result at a later time. So you could give that PendingResult to an AsyncTask to perform some longer running work, and then respond to the broadcast by calling methods on PendingResult. There are two downsides to this method. One is that it is not available on old devices. The other is that it is less flexible: you still have to service the broadcast within ten seconds or so, and you have few architectural options like you do with a service. Of course, goAsync() has one huge advantage: you can set results for ordered broadcasts with them. If you really need that, nothing else will do. Just make sure not to take too long. 499 This page intentionally left blank 31 Browsing The Web & WebView Each photo you get from Flickr has a page associated with it. In this chapter, you are going to make it so that users can press a photo in PhotoGallery to browse its photo page. You will learn two different ways to integrate web content into your apps. The first works with the device’s browser app, and the second uses the WebView class to display web content within PhotoGallery. One Last Bit of Flickr Data For both ways, you need to get the URL for a photo’s Flickr page. If you look at the XML you are currently receiving for each photo, you can see that the photo page is not part of those results.... So you might think that you are in for some more XML request writing. Fortunately, that is not the case. If you look at the Web Page URLs section at Flickr’s documentation at http://www.flickr.com/ services/api/misc.urls.html, you can see that you can create the URL for an individual photo’s page like so: http://www.flickr.com/photos/user-id/photo-id The photo-id seen here is the same as the value of the id attribute from your XML. You are already stashing that in mId in GalleryItem. What about user-id? If you poke around the documentation, you will find that the owner attribute in your XML is a user ID. So if you pull out the owner attribute, you should be able to build the URL from your photo XML attributes: http://www.flickr.com/photos/owner/id Add the following code to GalleryItem to put this plan into action. 501 Chapter 31 Browsing The Web & WebView Listing 31.1 Add code for photo page (GalleryItem.java) public class GalleryItem { private String mCaption; private String mId; private String mUrl; private String mOwner; ... public void setUrl(String url) { mUrl = url; } public String getOwner() { return mOwner; } public void setOwner(String owner) { mOwner = owner; } public String getPhotoPageUrl() { return "http://www.flickr.com/photos/" + mOwner + "/" + mId; } } public String toString() { return mCaption; } You created a new mOwner property here, and added a short method called getPhotoPageUrl() to generate photo page URLs as discussed above. Now change parseItems(…) to read in the owner attribute. Listing 31.2 Read in owner attribute (FlickrFetchr.java) void parseItems(ArrayList items, XmlPullParser parser) throws XmlPullParserException, IOException { int eventType = parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && XML_PHOTO.equals(parser.getName())) { String id = parser.getAttributeValue(null, "id"); String caption = parser.getAttributeValue(null, "title"); String smallUrl = parser.getAttributeValue(null, EXTRA_SMALL_URL); String owner = parser.getAttributeValue(null, "owner"); } } 502 } GalleryItem item = new GalleryItem(); item.setUrl(smallUrl); item.setOwner(owner); items.add(item); eventType = parser.next(); The Easy Way: Implicit Intents Easy peasy. Now to have fun with your new photo page URL. The Easy Way: Implicit Intents You will browse to this URL first by using your old friend the implicit intent. This intent will start up the browser with your photo URL. The first step here is to make your app listen to presses on GridView items. This is another place where your code will look slightly different from your code in Chapter 9, since there is no matching GridFragment. Instead of overriding the onListItemClick(…) method in your fragment, you will hook it up more like a button click listener by calling the setOnItemClickListener(…) method on your GridView. After doing that, it is a simple matter of creating and firing an implicit intent. Add the following code to PhotoGalleryFragment: Listing 31.3 Web browsing with implicit intents (PhotoGalleryFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false); mGridView = (GridView)v.findViewById(R.id.gridView); setupAdapter(); mGridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView> gridView, View view, int pos, long id) { GalleryItem item = mItems.get(pos); Uri photoPageUri = Uri.parse(item.getPhotoPageUrl()); Intent i = new Intent(Intent.ACTION_VIEW, photoPageUri); }); } } startActivity(i); return v; That should be it. Start up PhotoGallery and press on a photo. You should see your progress indicator briefly, and then your browser app should pop up. The Harder Way: WebView Oftentimes, though, you want to display web content within your own activities instead of heading off to the browser. You may want to display HTML that you generate yourself, or you may want to lock down the browser somehow. For apps that include help documentation, it is common to implement it as a web page so that it is easy to update. Opening a web browser to a help web page does not look professional, though, and it prevents you from customizing behavior or integrating that web page into your own user interface. 503 Chapter 31 Browsing The Web & WebView When you want to present web content within your own user interface, you use the WebView class. We are calling this the “harder” way here, but it is pretty darned easy. (Anything is hard compared to using implicit intents.) The first step will be to create a new activity and fragment to display the WebView in. Start, as usual, by defining a layout file. Figure 31.1 Initial layout (res/layout/fragment_photo_page.xml) You may think, “That RelativeLayout is pretty useless.” True enough – you will fill it out later in the chapter, though, with additional “chrome”. Next, get the rudiments of your fragment setup. Create PhotoPageFragment as a subclass of the VisibleFragment class you created in the last chapter. You will need to inflate your layout file, extract your WebView from it, and forward along the URL in your intent’s data. 504 The Harder Way: WebView Listing 31.4 Setting up your web browser fragment (PhotoPageFragment.java) package com.bignerdranch.android.photogallery; ... public class PhotoPageFragment extends VisibleFragment { private String mUrl; private WebView mWebView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } mUrl = getActivity().getIntent().getData().toString(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); mWebView = (WebView)v.findViewById(R.id.webView); } } return v; For now, this is just a skeleton, but leave it as it is – you will fill it out a bit more in a moment. Next, create the containing PhotoPageActivity class using good old SingleFragmentActivity. Listing 31.5 Create web activity (PhotoPageActivity.java) package com.bignerdranch.android.photogallery; ... public class PhotoPageActivity extends SingleFragmentActivity { @Override public Fragment createFragment() { return new PhotoPageFragment(); } } Switch up your code in PhotoGalleryFragment to call into your new activity instead of the implicit intent. 505 Chapter 31 Browsing The Web & WebView Listing 31.6 Switch to call your activity (PhotoGalleryFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... mGridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView> gridView, View view, int pos, long id) { GalleryItem item = mItems.get(pos); Uri photoPageUri = Uri.parse(item.getPhotoPageUrl()); Intent i = new Intent(Intent.ACTION_VIEW, photoPageUri); Intent i = new Intent(getActivity(), PhotoPageActivity.class); i.setData(photoPageUri); }); } startActivity(i); return v; } And, finally, add your new activity to the manifest. Listing 31.7 Add activity to manifest (AndroidManifest.xml) ... Run PhotoGallery and press on a picture. You should see a new empty activity pop up. 506 The Harder Way: WebView Okay, now to get to the meat and actually make your fragment do something. You need to do three things to make your WebView successfully display a Flickr photo page. The first one is straightforward – you need to tell it what URL to load. The second thing you need to do is enable JavaScript. By default, JavaScript is off. You do not always need to have it on, but for Flickr, you do. Android Lint gives you a warning for doing this (it is worried about cross-site scripting attacks), so you also need to suppress Lint warnings. Finally, you need to override one method on a class called WebViewClient, shouldOverrideUrlLoading(WebView,String), and return false. We will discuss this class a bit more after you type in the code. Listing 31.8 Add even more instance variables (PhotoPageFragment.java) @SuppressLint("SetJavaScriptEnabled") @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); mWebView = (WebView)v.findViewById(R.id.webView); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(new WebViewClient() { public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; } }); mWebView.loadUrl(mUrl); } return v; Loading the URL has to be done after configuring the WebView, so you do that last. Before that, you turn JavaScript on by calling getSettings() to get an instance of WebSettings, and calling WebSettings.setJavaScriptEnabled(true). WebSettings is the first of the three ways you can modify your WebView. It has various properties you can set, like the user agent string and text size. After that, you configure your WebViewClient. WebViewClient is an event interface. By providing your own implementation of WebViewClient, you can respond to rendering events. For example, you could detect when the renderer starts loading an image from a particular URL or decide whether to resubmit a POST request to the server. has many methods you can override, most of which you will not deal with. You do need to replace the default WebViewClient’s implementation of shouldOverrideUrlLoading(WebView,String), though. This method says what to do when a new URL is loaded in the WebView, like by pressing a link. If you return true, you are saying, “Do not handle this URL, I am handling it myself.” If you return false, you are saying, “Go ahead and load this URL, WebView, I’m not doing anything with it.” WebViewClient The default implementation fires an implicit intent with the URL, just like you did earlier this chapter. For your photo page, this is a severe problem. The first thing Flickr does is redirect you to the mobile version of the web site. With the default WebViewClient, that means that you are immediately sent to the user’s default web browser. Not ideal. 507 Chapter 31 Browsing The Web & WebView The fix is simple – just override the default implementation and return false. Run PhotoGallery, and you should see your WebView. Using WebChromeClient to spruce things up Since you are taking the time to create your own WebView, let’s spruce it up a bit by adding a title view and a progress bar. Crack open fragment_photo_page.xml and make the following changes: Figure 31.2 Add title and progress (fragment_photo_page.xml) Pulling out the ProgressBar and TextView will be a piece of cake. To hook them up, though, you will need to use the second callback on WebView: WebChromeClient. If WebViewClient is an interface for 508 Proper rotation with WebView responding to rendering events, then WebChromeClient is an event interface for reacting to events that should change elements of chrome around the browser. This includes JavaScript alerts, favicons, and of course updates for loading progress and the title of the current page. Hook it up in onCreateView(…) like so: Listing 31.9 Using WebChromeClient (PhotoPageFragment.java) @SuppressLint("SetJavaScriptEnabled") @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); final ProgressBar progressBar = (ProgressBar)v.findViewById(R.id.progressBar); progressBar.setMax(100); // WebChromeClient reports in range 0-100 final TextView titleTextView = (TextView)v.findViewById(R.id.titleTextView); mWebView = (WebView)v.findViewById(R.id.webView); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(new WebViewClient() { ... }); mWebView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView webView, int progress) { if (progress == 100) { progressBar.setVisibility(View.INVISIBLE); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setProgress(progress); } } }); public void onReceivedTitle(WebView webView, String title) { titleTextView.setText(title); } mWebView.loadUrl(mUrl); } return v; Progress updates and title updates each have their own callback method, onProgressChanged(WebView,int) and onReceivedTitle(WebView,String). The progress you receive from onProgressChanged(WebView,int) is an integer from 0 to 100. If it is 100, you know that the page is done loading, so you hide the ProgressBar by setting its visibility to View.INVISIBLE. Run PhotoGallery to test your changes. Proper rotation with WebView Try rotating your screen. While it does work correctly, you will notice that the WebView has to completely reload the web page. This is because WebView has too much data to save it all out inside onSaveInstanceState(…). It has to start from scratch each time it is recreated on rotation. 509 Chapter 31 Browsing The Web & WebView For some classes like this (VideoView is another one), the Android documentation recommends that you allow the activity to handle the configuration change itself. This means that instead of the activity being destroyed, it simply moves its views around to fit the new screen size. As a result, WebView does not have to reload all of its data. (Why not do this all the time, you ask? It does not work correctly with all views. If it did, life would be so much easier, but alas.) To tell PhotoPageActivity to handle its own darned configuration changes, make the following tweak to AndroidManifest.xml. Listing 31.10 Handle configuration changes yourself (AndroidManifest.xml)... ... ... This attribute says that if the configuration changes because the keyboard was opened or closed, due to an orientation change or due to the screen size changing (which also happens when switching between portrait and landscape after Android 3.2), then the activity should handle the change itself. And that is it. Try rotation again, and this time everything should be hunky dory. For the More Curious: Injecting JavaScript Objects We saw how to use WebViewClient and WebChromeClient to respond to specific events that happen in your WebView. However, it is possible to do even more than that by injecting arbitrary JavaScript objects into the document contained in the WebView itself. Check out the documentation at http:// developer.android.com/reference/android/webkit/WebView.html and scroll down to the addJavascriptInterface(Object,String) method. Using this, you can inject an arbitrary object into the document with a name you specify. mWebView.addJavascriptInterface(new Object() { public void send(String message) { Log.i(TAG, "Received message: " + message); } }, "androidObject"); And then invoke it like so: 510 For the More Curious: Injecting JavaScript Objects This could be dangerous – you’re letting some potentially strange web page fiddle with your program. So to be safe, it’s a good idea to make sure you own the HTML in question – either that, or be extremely conservative with the interface you expose. 511 This page intentionally left blank 32 Custom Views and Touch Events In this chapter, you will learn how to handle touch events by writing a custom subclass of View named BoxDrawingView. This View will draw boxes in response to the user touching the screen and dragging. Figure 32.1 Boxes drawn in many shapes and sizes Setting Up the DragAndDraw Project The BoxDrawingView class will be the star of a new project named DragAndDraw. Select New → Android�Application�Project. Configure the project as shown in Figure 32.2. Then create a blank activity named DragAndDrawActivity. 513 Chapter 32 Custom Views and Touch Events Figure 32.2 Creating DragAndDraw Setting up DragAndDrawActivity will be a subclass of SingleFragmentActivity that inflates the usual singlefragment-containing layout. In the package explorer, copy SingleFragmentActivity.java into the com.bignerdranch.android.draganddraw package. Then copy activity_fragment.xml into DragAndDraw’s res/layout directory. DragAndDrawActivity In DragAndDrawActivity.java, make DragAndDrawActivity a SingleFragmentActivity that creates a DragAndDrawFragment (a class that you will create next). Listing 32.1 Modify the activity (DragAndDrawActivity.java) public class DragAndDrawActivity extends Activity SingleFragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_drag_and_draw); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_drag_and_draw, menu); return true; } } 514 @Override public Fragment createFragment() { return new DragAndDrawFragment(); } Setting up DragAndDrawFragment Setting up DragAndDrawFragment To prepare a layout for DragAndDrawFragment, rename the activity_drag_and_draw.xml layout file to fragment_drag_and_draw.xml. DragAndDrawFragment’s layout will eventually consist of a BoxDrawingView, the custom view that you are going to write. All of the drawing and touch-event handling will be implemented in BoxDrawingView. Create a new class named DragAndDrawFragment and make its superclass android.support.v4.app.Fragment. Override onCreateView(…) to inflate fragment_drag_and_draw.xml. Listing 32.2 Create the fragment (DragAndDrawFragment.java) public class DragAndDrawFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_drag_and_draw, parent, false); return v; } } You can run DragAndDraw to confirm that your app is set up properly. Figure 32.3 DragAndDraw with default layout 515 Chapter 32 Custom Views and Touch Events Creating a Custom View Android provides many excellent standard views and widgets, but sometimes you need a custom view that presents visuals that are totally unique to your app. While there are all kinds of custom views, you can shoehorn them into two broad categories: simple A simple view may be complicated inside; what makes it simple is that it has no child views. A simple view will almost always perform custom rendering as well. composite Composite views are composed of other view objects. Composite views typically manage child views but do not perform custom rendering. Instead, rendering is delegated to each child view. Here are three steps to follow when creating a custom view: • Pick a superclass. For a simple custom view, View is a blank canvas, so it is the most common choice. For a composite custom view, choose an appropriate layout class. • Subclass this class and override at least one constructor from the superclass. Or create your own constructor that calls one of the superclass’s constructors. • Override other key methods to customize behavior. Creating BoxDrawingView BoxDrawingView will be a simple view and a direct subclass of View. Create a new class named BoxDrawingView and make View its superclass. In BoxDrawingView.java, add two constructors. Listing 32.3 Initial implementation for BoxDrawingView (BoxDrawingView.java) public class BoxDrawingView extends View { // Used when creating the view in code public BoxDrawingView(Context context) { this(context, null); } // Used when inflating the view from XML public BoxDrawingView(Context context, AttributeSet attrs) { super(context, attrs); } } You write two constructors because your view could be instantiated in code or from a layout file. Views instantiated from a layout file receive an instance of AttributeSet containing the XML attributes that were specified in XML. Even if you do not plan on using both constructors, it is good practice to include them. Next, update your fragment_drag_and_draw.xml layout file to use your new view. 516 Creating BoxDrawingView Listing 32.4 Add BoxDrawingView to layout (fragment_drag_and_draw.xml)... ... You must use BoxDrawingView’s fully qualified class name so that the layout inflater can find it. The inflater works through a layout file creating View instances. If the element name is an unqualified class name, then the inflater looks for a class with that name in the android.view and android.widget packages. If the class lives somewhere else, then the layout inflater will not find it, and your app will crash. So for custom classes and other classes that live outside of android.view and android.widget, you must always specify the fully qualified class name. Run DragAndDraw to confirm that all the connections are correct. All you will see is an empty view. 517 Chapter 32 Custom Views and Touch Events Figure 32.4 BoxDrawingView with no boxes The next step is to get BoxDrawingView listening for touch events and using the information they contain to draw boxes on the screen. Handling Touch Events One way to listen for touch events is to set a touch event listener using the following View method: public void setOnTouchListener(View.OnTouchListener l) This method works the same way as setOnClickListener(View.OnClickListener). You provide an implementation of View.OnTouchListener, and your listener will be called every time a touch event happens. However, because you are subclassing View, you can take a shortcut and override this View method: public boolean onTouchEvent(MotionEvent event) This method receives an instance of MotionEvent, a class that describes the touch event, including its location and its action. The action describes the stage of the event. action constants description ACTION_DOWN user’s finger touches the screen ACTION_MOVE user moves finger on the screen ACTION_UP user lifts finger off the screen 518 Tracking across motion events action constants description ACTION_CANCEL a parent view has intercepted your touch event In your implementation of onTouchEvent(…), you can check the value of the action by calling the MotionEvent method: public final int getAction() In BoxDrawingView.java, add a log tag and then an implementation of onTouchEvent(…) that logs a message in the case of each of the four different actions. Listing 32.5 Implementing BoxDrawingView (BoxDrawingView.java) public class BoxDrawingView extends View { public static final String TAG = "BoxDrawingView"; ... public boolean onTouchEvent(MotionEvent event) { PointF curr = new PointF(event.getX(), event.getY()); Log.i(TAG, "Received event at x=" + curr.x + ", y=" + curr.y + ":"); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, " ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, " ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG, " ACTION_UP"); break; case MotionEvent.ACTION_CANCEL: Log.i(TAG, " ACTION_CANCEL"); break; } } } return true; Notice that you package your X and Y coordinates in a PointF object. You will be wanting to pass these two values together as you go through the rest of the chapter. PointF is a container class provided by Android that does this for you. Run DragAndDraw and pull up LogCat. Touch the screen and drag your finger. You should see a report of the X and Y coordinate of every touch action that BoxDrawingView receives. Tracking across motion events is intended to draw boxes on the screen, not just log coordinates. There are a few problems to solve to get there. BoxDrawingView 519 Chapter 32 Custom Views and Touch Events First, to define a box, you need two things: • the origin point (where the finger was initially placed) • the current point (where the finger currently is) To define a box, then, requires keeping track of data from more than one MotionEvent. You will store this data in a Box object. Create a class named Box to represent the data that defines a single box. Listing 32.6 Add Box (Box.java) public class Box { private PointF mOrigin; private PointF mCurrent; public Box(PointF origin) { mOrigin = mCurrent = origin; } public PointF getCurrent() { return mCurrent; } public void setCurrent(PointF current) { mCurrent = current; } } public PointF getOrigin() { return mOrigin; } When the user touches BoxDrawingView, a new Box will be created and added to an array of existing boxes (Figure 32.5). Figure 32.5 Objects in DragAndDraw 520 Tracking across motion events Back in BoxDrawingView, add some code that uses your new Box object to track your drawing state. Listing 32.7 Add drag lifecycle methods (BoxDrawingView.java) public class BoxDrawingView extends View { public static final String TAG = "BoxDrawingView"; private Box mCurrentBox; private ArrayList mBoxes = new ArrayList (); ... public boolean onTouchEvent(MotionEvent event) { PointF curr = new PointF(event.getX(), event.getY()); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, " ACTION_DOWN"); // Reset drawing state mCurrentBox = new Box(curr); mBoxes.add(mCurrentBox); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, " ACTION_MOVE"); if (mCurrentBox != null) { mCurrentBox.setCurrent(curr); invalidate(); } break; case MotionEvent.ACTION_UP: Log.i(TAG, " ACTION_UP"); mCurrentBox = null; break; } } } case MotionEvent.ACTION_CANCEL: Log.i(TAG, " ACTION_CANCEL"); mCurrentBox = null; break; return true; Any time an ACTION_DOWN motion event is received, you set mCurrentBox to be a new Box with its origin as the event’s location. This new Box is added to the array of boxes. (In the next section, when you implement custom drawing, BoxDrawingView will draw every Box within this array to the screen.) As the user’s finger moves around the screen, you update mCurrentBox.mCurrent. Then, when the touch is canceled or when the user’s finger leaves the screen, you null out mCurrentBox to end your draw motion. The Box is complete; it is stored safely in the array but will no longer be updated about motion events. Notice the call to invalidate() in the case of ACTION_MOVE. This forces BoxDrawingView to redraw itself so that the user can see the box while dragging across the screen. Which brings you to the next step: drawing the boxes to the screen. 521 Chapter 32 Custom Views and Touch Events Rendering Inside onDraw(…) When your application is launched, all of its views are invalid. This means that they have not drawn anything to the screen. To fix this situation, Android calls the top-level View’s draw() method. This causes that view to draw itself, which causes its children to draw themselves. Those children’s children then draw themselves, and so on down the hierarchy. When all the views in the hierarchy have drawn themselves, then the top-level View is no longer invalid. To hook into this drawing, you override the following View method: protected void onDraw(Canvas canvas) The call to invalidate() that you make in response to ACTION_MOVE in onTouchEvent(…) makes the BoxDrawingView invalid again. This causes it to redraw itself and will cause onDraw(…) to be called again. Now let’s consider the Canvas parameter. Canvas and Paint are the two main drawing classes in Android: • The Canvas class has all the drawing operations you perform. The methods you call on Canvas determine where and what you draw – a line, a circle, a word, or a rectangle. • The Paint class determines how these operations are done. The methods you call on Paint specify characteristics – whether shapes are filled, which font text is drawn in, and what color lines are. In BoxDrawingView.java, create two Paint in BoxDrawingView’s XML constructor. Listing 32.8 Create your paint (BoxDrawingView.java) public class BoxDrawingView extends View { private static final String TAG = "BoxDrawingView"; private private private private ArrayList mBoxes = new ArrayList (); Box mCurrentBox; Paint mBoxPaint; Paint mBackgroundPaint; ... // Used when inflating the view from XML public BoxDrawingView(Context context, AttributeSet attrs) { super(context, attrs); // Paint the boxes a nice semitransparent red (ARGB) mBoxPaint = new Paint(); mBoxPaint.setColor(0x22ff0000); } } // Paint the background off-white mBackgroundPaint = new Paint(); mBackgroundPaint.setColor(0xfff8efe0); Armed with paint, you can now draw your boxes to the screen. 522 Rendering Inside onDraw(…) Listing 32.9 Overriding onDraw(Canvas) (BoxDrawingView.java) @Override protected void onDraw(Canvas canvas) { // Fill the background canvas.drawPaint(mBackgroundPaint); for (Box box : mBoxes) { float left = Math.min(box.getOrigin().x, box.getCurrent().x); float right = Math.max(box.getOrigin().x, box.getCurrent().x); float top = Math.min(box.getOrigin().y, box.getCurrent().y); float bottom = Math.max(box.getOrigin().y, box.getCurrent().y); } } canvas.drawRect(left, top, right, bottom, mBoxPaint); The first part of this code is straightforward: using your off-white background paint, you fill the canvas with a backdrop for your boxes. Then, for each box in your list of boxes, you determine what the left, right, top, and bottom of the box should be by looking at the two points for the box. The left and top values will be the minimum values, and the bottom and right will be the maximum values. After calculating these values, you call Canvas.drawRect(…) to draw a red rectangle onto the screen. Run DragAndDraw and draw some red rectangles. Figure 32.6 An expression of programmerly emotion 523 Chapter 32 Custom Views and Touch Events Challenge: Rotations Figure out how to persist your boxes across orientation changes from within your View. This can be done with the following View methods: protected Parcelable onSaveInstanceState() protected void onRestoreInstanceState(Parcelable state) These methods do not work like Activity and Fragment’s onSaveInstanceState(Bundle). First, they will only be called if your View has an id. Second, instead of taking in a Bundle, they return and process an object that implements the Parcelable interface. We recommend using a Bundle as the Parcelable instead of implementing a Parcelable class yourself. (Implementing the Parcelable interface is complicated. It is better to avoid doing so when possible.) For a harder challenge, make it so that you can use a second finger to rotate your rectangles. To do this, you will need to handle multiple pointers in your MotionEvent handling code. You will also need to rotate your canvas. When dealing with multiple touches, you need these extra ideas: pointer index tells you which pointer in the current set of pointers the event is for pointer ID gives you a unique ID for a specific finger in a gesture The pointer index may change, but the pointer ID will not. For more details, check out the documentation for the following MotionEvent methods: public public public public public final final final final final int getActionMasked() int getActionIndex() int getPointerId(int pointerIndex) float getX(int pointerIndex) float getY(int pointerIndex) Also look at the documentation for the ACTION_POINTER_UP and ACTION_POINTER_DOWN constants. 524 33 Tracking the Device’s Location In this chapter, you will create a new application called RunTracker. RunTracker works with a device’s GPS to record and display the user’s travels. Those travels may be a walk in the woods at Big Nerd Ranch Bootcamp, a car ride, or an ocean voyage. RunTracker is designed to keep a record of them all. The first version of RunTracker will simply get location updates from the GPS and display the device’s current location on screen. Eventually, RunTracker will show a map that follows the user in real time. Getting Started with RunTracker Create a new Android application with the following configuration: Figure 33.1 Creating RunTracker 525 Chapter 33 Tracking the Device’s Location Notice two differences from previous projects. First, you are raising the minimum SDK version to API 9. Second, you are building to the latest Google APIs rather than the Android version. You need the Google APIs to use maps. If you do not see a Google APIs version of the latest target listed, you can download it from the Android SDK Manager. Select Window → Android�SDK�Manager to pull up your SDK manager and then select the Google APIs, as shown in Figure 33.2. Then click Install�1�package. Figure 33.2 Installing Google APIs for SDK 4.2 Once installation is complete, you should see the Google APIs as an option in your SDK version selector. As with your other projects, have the wizard create a blank activity. Name it RunActivity. Setting up RunActivity RunActivity (and other activities in RunTracker) will use the SingleFragmentActivity class. Copy SingleFragmentActivity.java into the com.bignerdranch.android.runtracker package and activity_fragment.xml into res/layout/. Then, open RunActivity.java and change RunActivity to be a subclass of SingleFragmentActivity that hosts an instance of RunFragment. RunFragment does not exist yet, but you will create it shortly. Listing 33.1 Initial RunActivity (RunActivity.java) public class RunActivity extends SingleFragmentActivity { @Override protected Fragment createFragment() { return new RunFragment(); } } 526 Setting up RunFragment Setting up RunFragment The next steps are to create the user interface and the initial version of RunFragment. The UI for RunFragment (Figure 33.3) will be responsible for displaying simple data about the current “run” and its location. It will have buttons to start and stop tracking the current run. Figure 33.3 Initial RunTracker UI Adding strings First, let’s add resources for the strings shown in Figure 33.3. In res/values/strings.xml, add the following strings, including three that you will need later in the chapter. Listing 33.2 RunTracker strings (strings.xml) 527 Chapter 33 Tracking the Device’s Location Obtaining the layout file For the layout, you will use a TableLayout to keep everything tidy. The TableLayout will have five TableRows and one LinearLayout. Each TableRow will have two TextViews: the first is a label, and the second will be populated with data at runtime. The LinearLayout will contain two Buttons. This layout has nothing in it that you have not worked with before. Rather than create it from scratch, get this layout from the solutions file (http://www.bignerdranch.com/solutions/ AndroidProgramming.zip). Find 33_Location/RunTracker/res/layout/fragment_run.xml and copy it into your project’s res/layout directory. Creating the RunFragment class Now let’s create the RunFragment class itself. The initial version will just get the UI on the screen and provide access to its widgets. Listing 33.3 Skeleton of RunFragment (RunFragment.java) public class RunFragment extends Fragment { private Button mStartButton, mStopButton; private TextView mStartedTextView, mLatitudeTextView, mLongitudeTextView, mAltitudeTextView, mDurationTextView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_run, container, false); mStartedTextView = (TextView)view.findViewById(R.id.run_startedTextView); mLatitudeTextView = (TextView)view.findViewById(R.id.run_latitudeTextView); mLongitudeTextView = (TextView)view.findViewById(R.id.run_longitudeTextView); mAltitudeTextView = (TextView)view.findViewById(R.id.run_altitudeTextView); mDurationTextView = (TextView)view.findViewById(R.id.run_durationTextView); mStartButton = (Button)view.findViewById(R.id.run_startButton); mStopButton = (Button)view.findViewById(R.id.run_stopButton); } } return view; Run the app and confirm that it looks like Figure 33.3. Locations and the LocationManager With the foundation in place, now you can get to work on the inside of the house. Location data in Android is provided by the LocationManager system service. This service provides location updates to all applications who are interested in them, and it delivers those updates in one of two ways. 528 Locations and the LocationManager The first, and perhaps most straightforward, way is via the LocationListener interface. This interface gives you information about location updates (via onLocationChanged(Location)) as well as status updates and notifications that the provider has been enabled or disabled. Using LocationListener to get updates is a fine thing to do when you only need the location data to come to a single component in your application. For example, if you only wanted to display location updates within RunFragment, you could provide a LocationListener implementation to a call to one of LocationManager’s requestLocationUpdates(…) or requestSingleUpdate(…) methods and be done. In this case, however, you have bigger plans. The goal for RunTracker is to keep tracking the user’s location regardless of the state (or presence) of the user interface. You could do this using a sticky Service, but that brings its own headaches and is a bit heavyweight for this example. Instead, you will use the PendingIntent API introduced in Android 2.3 (Gingerbread). By requesting location updates with a PendingIntent, you are asking the LocationManager to send some kind of Intent for you in the future. So, your application components, and indeed your entire process, can die, and LocationManager will still deliver your intents until you tell it to stop, starting new components to respond to them as needed. You will take advantage of this to keep your app from consuming too many resources even while it is actively tracking the device’s location. To manage the communication with LocationManager, and eventually more details about the current run, create a singleton class called RunManager as shown in Listing 33.4. Listing 33.4 Skeleton of RunManager, a singleton (RunManager.java) public class RunManager { private static final String TAG = "RunManager"; public static final String ACTION_LOCATION = "com.bignerdranch.android.runtracker.ACTION_LOCATION"; private static RunManager sRunManager; private Context mAppContext; private LocationManager mLocationManager; // The private constructor forces users to use RunManager.get(Context) private RunManager(Context appContext) { mAppContext = appContext; mLocationManager = (LocationManager)mAppContext .getSystemService(Context.LOCATION_SERVICE); } public static RunManager get(Context c) { if (sRunManager == null) { // Use the application context to avoid leaking activities sRunManager = new RunManager(c.getApplicationContext()); } return sRunManager; } 529 Chapter 33 Tracking the Device’s Location private PendingIntent getLocationPendingIntent(boolean shouldCreate) { Intent broadcast = new Intent(ACTION_LOCATION); int flags = shouldCreate ? 0 : PendingIntent.FLAG_NO_CREATE; return PendingIntent.getBroadcast(mAppContext, 0, broadcast, flags); } public void startLocationUpdates() { String provider = LocationManager.GPS_PROVIDER; } // Start updates from the location manager PendingIntent pi = getLocationPendingIntent(true); mLocationManager.requestLocationUpdates(provider, 0, 0, pi); public void stopLocationUpdates() { PendingIntent pi = getLocationPendingIntent(false); if (pi != null) { mLocationManager.removeUpdates(pi); pi.cancel(); } } public boolean isTrackingRun() { return getLocationPendingIntent(false) != null; } } Notice that RunManager has three public instance methods. These are its basic API. It can start location updates, stop them, and tell you if it is currently tracking a run (which merely means that updates are currently registered with the LocationManager). In startLocationUpdates(), you specifically tell LocationManager to give you location updates via the GPS provider as frequently as possible. The requestLocationUpdates(String, long, float, PendingIntent) method expects parameters for the minimum time to wait (in milliseconds) and minimum distance to cover (in meters) before sending the next update. These parameters should be tuned to the maximum value you can withstand and still have your app provide a good user experience. For RunTracker, your user will want to know exactly where they are, and exactly where they have been, with as much accuracy as possible. This is why we hard-code the GPS provider and request updates as often as possible. On the other hand, for an app that is only vaguely interested in where a user is, larger values will do just fine and will save the device’s battery from melting into hot slag. The private getLocationPendingIntent(boolean) method creates an Intent to be broadcast when location updates happen. You use a custom action name to identify the event within our app, and the shouldCreate argument to tell PendingIntent.getBroadcast(…) (via flags) whether it should create a new PendingIntent in the system or not. Calling getLocationPendingIntent(false) and null-checking the result is therefore a way of determining whether the PendingIntent is registered with the OS, and is therefore the implementation of the isTrackingRun() method. 530 Receiving Broadcast Location Updates Receiving Broadcast Location Updates Now that you have code to request location updates as broadcast Intents, you need to have a way to receive them. RunTracker will need to be able to receive them whether or not any UI components, or even the application process, are running, so the best place to handle it is with a standalone BroadcastReceiver registered in the manifest. To keep things simple, create LocationReceiver to log out the locations it receives. Listing 33.5 Basic LocationReceiver (LocationReceiver.java) public class LocationReceiver extends BroadcastReceiver { private static final String TAG = "LocationReceiver"; @Override public void onReceive(Context context, Intent intent) { // If you got a Location extra, use it Location loc = (Location)intent .getParcelableExtra(LocationManager.KEY_LOCATION_CHANGED); if (loc != null) { onLocationReceived(context, loc); return; } // If you get here, something else has happened if (intent.hasExtra(LocationManager.KEY_PROVIDER_ENABLED)) { boolean enabled = intent .getBooleanExtra(LocationManager.KEY_PROVIDER_ENABLED, false); onProviderEnabledChanged(enabled); } } protected void onLocationReceived(Context context, Location loc) { Log.d(TAG, this + " Got location from " + loc.getProvider() + ": " + loc.getLatitude() + ", " + loc.getLongitude()); } protected void onProviderEnabledChanged(boolean enabled) { Log.d(TAG, "Provider " + (enabled ? "enabled" : "disabled")); } } As you can see in the implementation of onReceive(Context, Intent), the LocationManager will pack the intent with some extras of interest. The LocationManager.KEY_LOCATION_CHANGED key may specify a Location instance that represents the latest update. If it returns one, then call the onLocationReceived(Context, Location) method to log out the provider name, latitude, and longitude. LocationManager may also pass a boolean extra with the KEY_PROVIDER_ENABLED key, and if it does, call the onProviderEnabled(boolean) method to log about that. Eventually, you will subclass LocationReceiver to make these two methods do more useful things. Add an entry for LocationReceiver in the manifest for RunTracker. While you’re at it, add the ACCESS_FINE_LOCATION permission and a uses-feature element for the GPS hardware. 531 Chapter 33 Tracking the Device’s Location Listing 33.6 Adding location permission (AndroidManifest.xml) RunTracker Started: Latitude: Longitude: Altitude: Elapsed Time: Start Stop GPS Enabled GPS Disabled Run at %1$s Now you have enough plumbing in place to request and receive location updates, and all that remains is to provide some user interface to start, stop, and display them. Updating the UI with Location Data Just to verify that things are working as expected, add click listeners to the Start and Stop buttons in RunFragment that communicate with RunManager. Also add calls to a simple updateUI() method. Listing 33.7 Starting and stopping location updates (RunFragment.java) public class RunFragment extends Fragment { private RunManager mRunManager; private Button mStartButton, mStopButton; private TextView mStartedTextView, mLatitudeTextView, mLongitudeTextView, mAltitudeTextView, mDurationTextView; 532 Updating the UI with Location Data @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mRunManager = RunManager.get(getActivity()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... mStartButton = (Button)view.findViewById(R.id.run_startButton); mStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRunManager.startLocationUpdates(); updateUI(); } }); mStopButton = (Button)view.findViewById(R.id.run_stopButton); mStopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRunManager.stopLocationUpdates(); updateUI(); } }); updateUI(); } return view; private void updateUI() { boolean started = mRunManager.isTrackingRun(); } } mStartButton.setEnabled(!started); mStopButton.setEnabled(started); With these additions, you can run RunTracker again and see location updates coming in via LogCat. For best results, use the Emulator Control window in DDMS to send fake updates to an emulator, or take your device outside and wait for a GPS fix. It can take several minutes to get the first update. Skip ahead a couple of pages for more details on providing test locations if you’re impatient or locked inside your secret underground programming bunker. Logging to LogCat is not a very user-friendly way of reporting locations. To get something on the screen, you can implement a subclass of LocationReceiver in RunFragment that will stash away the Location and update the UI. With a bit more data, stored in a new Run instance, you can display the start date and duration of the current run. Start by implementing a simple Run class that holds its start date and knows how to calculate its duration and format it as a string. 533 Chapter 33 Tracking the Device’s Location Listing 33.8 Your basic Run (Run.java) public class Run { private Date mStartDate; public Run() { mStartDate = new Date(); } public Date getStartDate() { return mStartDate; } public void setStartDate(Date startDate) { mStartDate = startDate; } public int getDurationSeconds(long endMillis) { return (int)((endMillis - mStartDate.getTime()) / 1000); } public static String formatDuration(int durationSeconds) { int seconds = durationSeconds % 60; int minutes = ((durationSeconds - seconds) / 60) % 60; int hours = (durationSeconds - (minutes * 60) - seconds) / 3600; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } } Now make use of the Run class with some updates to RunFragment. Listing 33.9 Displaying location updates (RunFragment.java) public class RunFragment extends Fragment { private BroadcastReceiver mLocationReceiver = new LocationReceiver() { @Override protected void onLocationReceived(Context context, Location loc) { mLastLocation = loc; if (isVisible()) updateUI(); } @Override protected void onProviderEnabledChanged(boolean enabled) { int toastText = enabled ? R.string.gps_enabled : R.string.gps_disabled; Toast.makeText(getActivity(), toastText, Toast.LENGTH_LONG).show(); } }; private RunManager mRunManager; private Run mRun; private Location mLastLocation; 534 Updating the UI with Location Data private Button mStartButton, mStopButton; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... mStartButton = (Button)view.findViewById(R.id.run_startButton); mStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRunManager.startLocationUpdates(); mRun = new Run(); updateUI(); } }); } ... @Override public void onStart() { super.onStart(); getActivity().registerReceiver(mLocationReceiver, new IntentFilter(RunManager.ACTION_LOCATION)); } @Override public void onStop() { getActivity().unregisterReceiver(mLocationReceiver); super.onStop(); } private void updateUI() { boolean started = mRunManager.isTrackingRun(); if (mRun != null) mStartedTextView.setText(mRun.getStartDate().toString()); int durationSeconds = 0; if (mRun != null && mLastLocation != null) { durationSeconds = mRun.getDurationSeconds(mLastLocation.getTime()); mLatitudeTextView.setText(Double.toString(mLastLocation.getLatitude())); mLongitudeTextView.setText(Double.toString(mLastLocation.getLongitude())); mAltitudeTextView.setText(Double.toString(mLastLocation.getAltitude())); } mDurationTextView.setText(Run.formatDuration(durationSeconds)); } mStartButton.setEnabled(!started); mStopButton.setEnabled(started); } There are a few things happening here. Foremost, there are new instance variables for a Run and the last Location received. These are the data that back up the user interface updates performed in updateUI(). The Run gets initialized as soon as you start location updates. 535 Chapter 33 Tracking the Device’s Location You create an anonymous LocationReceiver class and stash it in mLocationReceiver to save the location received and update the UI. You also display a Toast when the GPS provider is enabled or disabled. Finally, the implementations of the onStart() and onStop() methods are used to register and unregister the receiver in conjunction with the fragment being visible to the user. It would also be sensible to do this work in onCreate(Bundle) and onDestroy(), so that the mLastLocation variable would always contain the latest location update, even if the fragment was offscreen while it was received. Run RunTracker again, and you should now see actual location details populating the user interface. Faster Answers: the Last Known Location There are times when your users do not want to sit and wait for several minutes for their device to communicate with mysterious satellites in space just to find out where they are. Fortunately, with a little more work, you can prevent them from having to wait by using the LocationManager’s last known location for any location provider. Since you are using the GPS provider only, it makes the most sense to ask for its last known location, and doing so is rather straightforward. The only other trick is getting that location back to the user interface, and for that you can simply broadcast an Intent just as though you were the LocationManager. Listing 33.10 Getting the last known location (RunManager.java) public void startLocationUpdates() { String provider = LocationManager.GPS_PROVIDER; // Get the last known location and broadcast it if you have one Location lastKnown = mLocationManager.getLastKnownLocation(provider); if (lastKnown != null) { // Reset the time to now lastKnown.setTime(System.currentTimeMillis()); broadcastLocation(lastKnown); } } // Start updates from the location manager PendingIntent pi = getLocationPendingIntent(true); mLocationManager.requestLocationUpdates(provider, 0, 0, pi); private void broadcastLocation(Location location) { Intent broadcast = new Intent(ACTION_LOCATION); broadcast.putExtra(LocationManager.KEY_LOCATION_CHANGED, location); mAppContext.sendBroadcast(broadcast); } Notice that you are resetting the timestamp of the location you get from the GPS provider. This may or may not be what the user expects, and determining that is left as an exercise to the reader. It is also possible to ask the LocationManager for the last known location from any provider it is aware of. You can even ask it what providers it knows about using the getAllProviders() method. If you do 536 Testing Locations on Real and Virtual Devices iterate over the last known locations, you should check them for accuracy and see that their timestamp is within the recent past. If they are not fresh, you may not want to use them. Testing Locations on Real and Virtual Devices Testing an app like RunTracker can be challenging, even for an avid outdoors-programmer. You want to ensure that the locations you receive from the system are being appropriately tracked and stored. This can be difficult to do if you are moving around, even at the low speeds achieved by having your friend haul you and your development setup around the neighborhood on a bicycle trailer. To get around situations like this, you can send test locations to the LocationManager that will allow your device to pretend that it is somewhere else. The simplest way to make this happen is using the Emulator Control window within DDMS. This only works with virtual devices, but it allows you to specify new locations either manually, one at a time, or with a GPX or KML file representing a series of locations visited over time. For testing locations on a real device, you have a bit more work to do, but it is entirely possible. The basic process looks like this: 1. Request the ACCESS_MOCK_LOCATION permission. 2. Add a test provider via LocationManager.addTestProvider(…). 3. Enable the provider using setTestProviderEnabled(…). 4. Set its initial status with setTestProviderStatus(…). 5. Publish locations with setTestProviderLocation(…). 6. Remove your test provider using removeTestProvider(…). Fortunately, we have already done all the hard work for you. Big Nerd Ranch has a simple TestProvider project that you can download, install to your device, and run to manage the test provider and make it run. Download the Android Course Resources repository from Github at https://github.com/ bignerdranch/AndroidCourseResources and import the TestProvider directory as a project in Eclipse. You will need to add some code to RunTracker to use the new test provider instead of GPS. Update RunManager as shown below to make that possible. 537 Chapter 33 Tracking the Device’s Location Listing 33.11 Using a test provider (RunManager.java) public class RunManager { private static final String TAG = "RunManager"; public static final String ACTION_LOCATION = "com.bignerdranch.android.runtracker.ACTION_LOCATION"; private static final String TEST_PROVIDER = "TEST_PROVIDER"; private static RunManager sRunManager; private Context mAppContext; private LocationManager mLocationManager; ... public void startLocationUpdates() { String provider = LocationManager.GPS_PROVIDER; // If you have the test provider and it's enabled, use it if (mLocationManager.getProvider(TEST_PROVIDER) != null && mLocationManager.isProviderEnabled(TEST_PROVIDER)) { provider = TEST_PROVIDER; } Log.d(TAG, "Using provider " + provider); // get the last known location and broadcast it if you have one Location lastKnown = mLocationManager.getLastKnownLocation(provider); if (lastKnown != null) { // Reset the time to now lastKnown.setTime(System.currentTimeMillis()); broadcastLocation(lastKnown); } In order for TestProvider to work, you may need to turn on the Allow�mock�locations setting in the Developer�options menu from within the Settings application. See Figure 33.4. With that out of the way, run the TestProvider app on your device and press the button to start fake location updates. Then you can run RunTracker and see your faked data coming through. (Hint: it’s in Atlanta, Georgia, USA.) Once you are finished testing, it is best to turn off the test location provider so that your device is not confused about where it is. 538 Testing Locations on Real and Virtual Devices Figure 33.4 Allow mock locations 539 This page intentionally left blank 34 Local Databases with SQLite Applications with large or complex data sets often need more power than you can get from simple file formats like JSON. In RunTracker, the user can continue tracking his or her location forever, which can generate a lot of data. The logical choice for such data sets in Android is a SQLite database. SQLite is an open source, multi-platform library that gives you access to a powerful, relational database API backed by a single file on disk. Android includes a Java front-end to SQLite through the SQLiteDatabase class, which provides result sets as Cursor instances. In this chapter you will create a storage mechanism for RunTracker that uses a database to store data about runs and their locations. You will also create a new run list activity and fragment that allow the user to create and track multiple runs. Storing Runs and Locations in a Database In order to store anything in a database, first you need to define the structure of the database and open it. Since this is such a common task in Android, a helper class exists. SQLiteOpenHelper encapsulates the chore of creating, opening, and updating databases for storing your application data. In RunTracker, you will create a subclass of SQLiteOpenHelper called RunDatabaseHelper. The RunManager will hold on to a private instance of RunDatabaseHelper and provide the rest of the application with an API for inserting, querying, and otherwise managing the data in the database. RunDatabaseHelper will provide methods that RunManager will call to implement most of its API. When designing an application’s database storage API, you typically create one subclass of SQLiteOpenHelper for each type of database you need to create and manage. You then create one instance of your subclass for each distinct SQLite database file you need to access. Most applications, including RunTracker, will have just one subclass of SQLiteOpenHelper and share a single instance of it with the rest of the application components. Now that you know you need to create a database, consider the structure of it. In object oriented programming, the most common pattern for database design is to use one database table for each class in your application’s data model. For RunTracker, there are two classes to store: Run and Location. This example will therefore create two tables: run and location. A Run can have many Locations, so the location table will have a run_id foreign key column referencing the run table’s _id column. Figure 34.1 shows the structure of the tables. 541 Chapter 34 Local Databases with SQLite Figure 34.1 RunTracker database schema Create the RunDatabaseHelper class by adding the code shown in Listing 34.1. Listing 34.1 Basic RunDatabaseHelper (RunDatabaseHelper.java public class RunDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "runs.sqlite"; private static final int VERSION = 1; private static final String TABLE_RUN = "run"; private static final String COLUMN_RUN_START_DATE = "start_date"; public RunDatabaseHelper(Context context) { super(context, DB_NAME, null, VERSION); } @Override public void onCreate(SQLiteDatabase db) { // Create the "run" table db.execSQL("create table run (" + "_id integer primary key autoincrement, start_date integer)"); // Create the "location" table db.execSQL("create table location (" + " timestamp integer, latitude real, longitude real, altitude real," + " provider varchar(100), run_id integer references run(_id))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Implement schema changes and data massage here when upgrading } } 542 public long insertRun(Run run) { ContentValues cv = new ContentValues(); cv.put(COLUMN_RUN_START_DATE, run.getStartDate().getTime()); return getWritableDatabase().insert(TABLE_RUN, null, cv); } Storing Runs and Locations in a Database Implementing a subclass of SQLiteOpenHelper requires you to override two methods: onCreate(SQLiteDatabase) and onUpgrade(SQLiteDatabase, int, int). In onCreate(…) your job is to establish the schema for a newly created database. In onUpgrade(…) you have the opportunity to execute migration code to move from one version of the database schema to another. It is also common to implement a simplified constructor that fills in some of the arguments required by the superclass version. In this example, you pass a constant name for the database file, a null value for the optional CursorFactory, and a constant integer version number. While it will not be necessary for the RunTracker example, SQLiteOpenHelper supports the ability to manage different versions of a database schema. It expects the version number to be an increasing integer value starting at one. In a real application, each time you made changes to your database schema, you would increment the version constant and write code in the onUpgrade(…) method to handle any schema or data changes that were necessary between versions. Here, you have implemented onCreate(…) to execute two CREATE TABLE SQL statements on the freshly-created database. You also implemented the insertRun(Run) method to insert a new row in the run table and return its ID. Runs have a single data field, their start date, and here you are storing the long value of it in the database using a ContentValues object to represent the mapping of column names to values. SQLiteOpenHelper has two methods that will give you access to an instance of SQLiteDatabase: getWritableDatabase() and getReadableDatabase(). The pattern used in this example uses getWritableDatabase() when a writable database is needed, and getReadableDatabase() when only read access is required. In practice, the implementations of these methods will return the same instance of SQLiteDatabase for a given instance of SQLiteOpenHelper, but in certain rare situations, like a full disk, you can get a readable database when you might not be able to get a writable one. To support querying one or more runs from the database, and distinguishing them in the app, you will need to add an ID property to the Run class. Update it with the changes shown in Listing 34.2. Listing 34.2 Adding an ID to Run (Run.java) public class Run { private long mId; private Date mStartDate; public Run() { mId = -1; mStartDate = new Date(); } public long getId() { return mId; } public void setId(long id) { mId = id; } public Date getStartDate() { return mStartDate; } 543 Chapter 34 Local Databases with SQLite Next, you need to make some enhancements to RunManager that make use of the new database. This will be the API that the rest of the application uses to store and retrieve data. To start, add just enough code to get Run storage working. Listing 34.3 Managing the current run (RunManager.java) public class RunManager { private static final String TAG = "RunManager"; private static final String PREFS_FILE = "runs"; private static final String PREF_CURRENT_RUN_ID = "RunManager.currentRunId"; public static final String ACTION_LOCATION = "com.bignerdranch.android.runtracker.ACTION_LOCATION"; private static final String TEST_PROVIDER = "TEST_PROVIDER"; private private private private private private static RunManager sRunManager; Context mAppContext; LocationManager mLocationManager; RunDatabaseHelper mHelper; SharedPreferences mPrefs; long mCurrentRunId; private RunManager(Context appContext) { mAppContext = appContext; mLocationManager = (LocationManager)mAppContext .getSystemService(Context.LOCATION_SERVICE); mHelper = new RunDatabaseHelper(mAppContext); mPrefs = mAppContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); mCurrentRunId = mPrefs.getLong(PREF_CURRENT_RUN_ID, -1); } ... private void broadcastLocation(Location location) { Intent broadcast = new Intent(ACTION_LOCATION); broadcast.putExtra(LocationManager.KEY_LOCATION_CHANGED, location); mAppContext.sendBroadcast(broadcast); } public Run startNewRun() { // Insert a run into the db Run run = insertRun(); // Start tracking the run startTrackingRun(run); return run; } public void startTrackingRun(Run run) { // Keep the ID mCurrentRunId = run.getId(); // Store it in shared preferences mPrefs.edit().putLong(PREF_CURRENT_RUN_ID, mCurrentRunId).commit(); // Start location updates startLocationUpdates(); } 544 Storing Runs and Locations in a Database public void stopRun() { stopLocationUpdates(); mCurrentRunId = -1; mPrefs.edit().remove(PREF_CURRENT_RUN_ID).commit(); } } private Run insertRun() { Run run = new Run(); run.setId(mHelper.insertRun(run)); return run; } Take a look at the new methods you just added to RunManager. The startNewRun() method calls insertRun() to create and insert a new Run into the database, passes it to startTrackingRun(Run) to begin tracking it, and finally returns it to the caller. You will use this method in RunFragment in response to the Start button when there is no existing run to work with. will also use startTrackingRun(Run) directly when it restarts tracking on an existing run. This method saves the ID of the Run passed to it in both an instance variable and in shared preferences. Storing it this way allows it to be retrieved later, even if the app is killed completely; the RunManager constructor will do this work in that event. RunFragment Finally, the stopRun() method stops location updates and clears out the ID of the current run. RunFragment will use this method to implement the Stop button. Speaking of RunFragment, now is a good time to make use of the new RunManager methods there. Make the changes in Listing 34.4. 545 Chapter 34 Local Databases with SQLite Listing 34.4 Updating the starting and stopping code (RunFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_run, container, false); ... mStartButton = (Button)view.findViewById(R.id.run_startButton); mStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRunManager.startLocationUpdates(); mRun = new Run(); mRun = mRunManager.startNewRun(); updateUI(); } }); mStopButton = (Button)view.findViewById(R.id.run_stopButton); mStopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRunManager.stopLocationUpdates(); mRunManager.stopRun(); updateUI(); } }); updateUI(); } return view; Next, you need the power to insert Location objects into the database in response to updates from the LocationManager. Similar to inserting Runs, you will add a method to both RunDatabaseHelper and RunManager to insert a location for the current run. Unlike inserting a Run, however, RunTracker needs to be able to insert locations as updates arrive regardless of whether the user interface is visible or the application is running. To handle this requirement, a standalone BroadcastReceiver is the best option. First, add the insertLocation(long, 546 Location) method in RunDatabaseHelper: Storing Runs and Locations in a Database Listing 34.5 Inserting locations in the database (RunDatabaseHelper.java) public class RunDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "runs.sqlite"; private static final int VERSION = 1; private static final String TABLE_RUN = "run"; private static final String COLUMN_RUN_START_DATE = "start_date"; private private private private private private private static static static static static static static final final final final final final final String String String String String String String TABLE_LOCATION = "location"; COLUMN_LOCATION_LATITUDE = "latitude"; COLUMN_LOCATION_LONGITUDE = "longitude"; COLUMN_LOCATION_ALTITUDE = "altitude"; COLUMN_LOCATION_TIMESTAMP = "timestamp"; COLUMN_LOCATION_PROVIDER = "provider"; COLUMN_LOCATION_RUN_ID = "run_id"; ... public long insertLocation(long runId, Location location) { ContentValues cv = new ContentValues(); cv.put(COLUMN_LOCATION_LATITUDE, location.getLatitude()); cv.put(COLUMN_LOCATION_LONGITUDE, location.getLongitude()); cv.put(COLUMN_LOCATION_ALTITUDE, location.getAltitude()); cv.put(COLUMN_LOCATION_TIMESTAMP, location.getTime()); cv.put(COLUMN_LOCATION_PROVIDER, location.getProvider()); cv.put(COLUMN_LOCATION_RUN_ID, runId); return getWritableDatabase().insert(TABLE_LOCATION, null, cv); } Now, add code to RunManager to insert a location for the currently tracking run. Listing 34.6 Inserting a location for the current run (RunManager.java) private Run insertRun() { Run run = new Run(); run.setId(mHelper.insertRun(run)); return run; } } public void insertLocation(Location loc) { if (mCurrentRunId != -1) { mHelper.insertLocation(mCurrentRunId, loc); } else { Log.e(TAG, "Location received with no tracking run; ignoring."); } } Lastly, you need a good place to call the new insertLocation(Location) method. By using a standalone BroadcastReceiver for this work, you can guarantee that your location intents will be handled no matter whether the rest of RunTracker is up and running. This requires creating a customized subclass of LocationReceiver called TrackingLocationReceiver and registering it using an intent filter in the manifest. Create TrackingLocationReceiver as a top-level class using the following code: 547 Chapter 34 Local Databases with SQLite Listing 34.7 Simple, yet elegant TrackingLocationReceiver (TrackingLocationReceiver.java) public class TrackingLocationReceiver extends LocationReceiver { @Override protected void onLocationReceived(Context c, Location loc) { RunManager.get(c).insertLocation(loc); } } Now register it in the manifest to make it execute in response to the custom ACTION_LOCATION. Listing 34.8 Enabling TrackingLocationReceiver (AndroidManifest.xml) ... After so many changes, you deserve to run the application and see your hard work pay off. RunTracker is now capable of tracking a run until you tell it to stop, even if the app is killed or exited. You can verify that things are working as expected by adding a log statement to the success path of TrackingLocationReceiver’s onLocationReceived(…) method, starting a run, then killing or leaving the app while continuing to watch LogCat. Querying a List of Runs From the Database RunTracker can now insert new runs and locations for them into the database, but as written, RunFragment will create a new run each time you press the Start button. In this section, you will add a new activity and fragment to display a list of runs and to allow the user to create new runs and view existing runs. This is a similar interface to what you implemented in CriminalIntent for the list of crimes, except that the data backing the list will be coming from a database instead of RunTracker’s memory and file data storage mechanism. Querying a SQLiteDatabase returns an instance of Cursor describing the results. The Cursor API is simple and flexible enough to support any kind of result from any query. Cursors treat their results as a series of rows and columns, and only support Strings and primitive types for values. 548 Querying a List of Runs From the Database As Java programmers, however, we are used to working with objects to encapsulate our data model, such as Run and Location. Since you already have database tables that represent your objects, it would be ideal if you could get instances of those objects from a Cursor. A pattern you will use in this chapter takes advantage of a built-in subclass of Cursor called CursorWrapper. CursorWrapper is designed to wrap an existing Cursor and forward along all of the method calls to it. On its own, it is not very useful; but as a superclass, it can provide you with a good foundation on which to build your own custom cursor implementations specific for your model objects. Update RunDatabaseHelper to include a new queryRuns() method that returns a RunCursor listing all the runs in order by date - an example of the CursorWrapper pattern. Listing 34.9 Querying runs (RunDatabaseHelper.java) public class RunDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "runs.sqlite"; private static final int VERSION = 1; private static final String TABLE_RUN = "run"; private static final String COLUMN_RUN_ID = "_id"; private static final String COLUMN_RUN_START_DATE = "start_date"; ... public RunCursor queryRuns() { // Equivalent to "select * from run order by start_date asc" Cursor wrapped = getReadableDatabase().query(TABLE_RUN, null, null, null, null, null, COLUMN_RUN_START_DATE + " asc"); return new RunCursor(wrapped); } /** * A convenience class to wrap a cursor that returns rows from the "run" table. * The {@link getRun()} method will give you a Run instance representing * the current row. */ public static class RunCursor extends CursorWrapper { public RunCursor(Cursor c) { super(c); } } } /** * Returns a Run object configured for the current row, * or null if the current row is invalid. */ public Run getRun() { if (isBeforeFirst() || isAfterLast()) return null; Run run = new Run(); long runId = getLong(getColumnIndex(COLUMN_RUN_ID)); run.setId(runId); long startDate = getLong(getColumnIndex(COLUMN_RUN_START_DATE)); run.setStartDate(new Date(startDate)); return run; } 549 Chapter 34 Local Databases with SQLite defines only two methods: a simple constructor and getRun(). The getRun() method checks to ensure that the cursor is within its bounds and then creates and configures an instance of Run based on the values of the current row’s columns. A user of RunCursor would iterate over the rows in the result set and call getRun() for each row to get a nice object instead of a bunch of nasty primitives. RunCursor Thus, RunCursor’s main purpose is to encapsulate the grunt work of turning a row in the run table into an instance of Run, marshaling and massaging the data as needed. The queryRuns() method does the work of executing the SQL query and providing the plain cursor to a new RunCursor, which it returns to the caller. Now you can make use of this new method in RunManager and eventually RunListFragment. Listing 34.10 Proxying run queries (RunManager.java) private Run insertRun() { Run run = new Run(); run.setId(mHelper.insertRun(run)); return run; } public RunCursor queryRuns() { return mHelper.queryRuns(); } public void insertLocation(Location loc) { if (mCurrentRunId != -1) { mHelper.insertLocation(mCurrentRunId, loc); } else { Log.e(TAG, "Location received with no tracking run; ignoring."); } } Displaying a List of Runs Using CursorAdapter To set the ground work for a user interface listing the runs, create a new default activity for the app called RunListActivity and set it as the default in the manifest. Ignore the error about RunListFragment for the moment. Listing 34.11 The great and powerful RunListActivity (RunListActivity.java) public class RunListActivity extends SingleFragmentActivity { @Override protected Fragment createFragment() { return new RunListFragment(); } } 550 Displaying a List of Runs Using CursorAdapter Listing 34.12 Configuring RunListActivity (AndroidManifest.xml)Now you can create the basic skeleton of RunListFragment. For now, you are loading the cursor in onCreate(Bundle) and closing it in onDestroy(), but this is not a good pattern to follow, because it forces the database query to happen on the main (UI) thread and in extreme cases could cause a dreaded ANR to appear. In the next chapter, you will swap out the native implementation for one that uses a Loader to move the work to the background. Listing 34.13 Basic RunListFragment (RunListFragment.java) public class RunListFragment extends ListFragment { private RunCursor mCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Query the list of runs mCursor = RunManager.get(getActivity()).queryRuns(); } @Override public void onDestroy() { mCursor.close(); super.onDestroy(); } } A RunCursor is no good without a way to provide its data to the ListView associated with RunListFragment. Included in the Android API (and the support library) is a class called CursorAdapter that fits the bill nicely. All you need to do is subclass it and provide implementations of a couple of methods. CursorAdapter handles the logic of creating and reusing views, so you don’t have to. Update RunListFragment to implement RunCursorAdapter now. 551 Chapter 34 Local Databases with SQLite Listing 34.14 Implementing RunCursorAdapter (RunListFragment.java) public class RunListFragment extends ListFragment { private RunCursor mCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Query the list of runs mCursor = RunManager.get(getActivity()).queryRuns(); // Create an adapter to point at this cursor RunCursorAdapter adapter = new RunCursorAdapter(getActivity(), mCursor); setListAdapter(adapter); } @Override public void onDestroy() { mCursor.close(); super.onDestroy(); } private static class RunCursorAdapter extends CursorAdapter { private RunCursor mRunCursor; public RunCursorAdapter(Context context, RunCursor cursor) { super(context, cursor, 0); mRunCursor = cursor; } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { // Use a layout inflater to get a row view LayoutInflater inflater = (LayoutInflater)context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater .inflate(android.R.layout.simple_list_item_1, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { // Get the run for the current row Run run = mRunCursor.getRun(); } } } // Set up the start date text view TextView startDateTextView = (TextView)view; String cellText = context.getString(R.string.cell_text, run.getStartDate()); startDateTextView.setText(cellText); The constructor for CursorAdapter takes a Context, a Cursor, and an integer of flags. Most of the flags are now deprecated or questionable in favor of using loaders, so here you pass zero. You also stash the RunCursor in an instance variable to avoid having to cast it later. 552 Creating New Runs Next, you implement newView(Context, Cursor, ViewGroup) to return a View to represent the current row in the cursor. This example inflates the system resource android.R.layout.simple_list_item_1, which is a plain TextView. Since all of the views in the list will look the same, that is all the logic you need here. The bindView(View, Context, Cursor) method will be called by CursorAdapter when it wants you to configure a view to hold data for a row in the cursor. This will always be called with a View that has been previously returned from newView(…). Implementing bindView(…) is relatively simple. First, you ask the RunCursor for the Run at the current row (the cursor will have already been positioned by CursorAdapter). Next, you assume that the view passed in is a TextView and configure it to display a simple description of the Run. With these changes in place, run RunTracker; you should see a list of the runs you have previously created, assuming you ran it and started a run after implementing the database insert code earlier in the chapter. If you don’t yet have any runs, fear not: the next section has you add UI to create a new one. Creating New Runs Adding a user interface to allow creation of new runs is easy using an options/action bar menu item, just as you did with CriminalIntent. Start by creating a resource for the menu. Listing 34.15 Options menu for the run list (run_list_options.xml) This menu refers to a string, so add that to the strings file. Listing 34.16 Adding a New Run string (strings.xml) Stop GPS Enabled GPS Disabled Run at %1$s New Run Now, add code to RunListFragment to create the options menu and respond to item selection, as shown below. 553 Chapter 34 Local Databases with SQLite Listing 34.17 New runs via the options menu (RunListFragment.java) public class RunListFragment extends ListFragment { private static final int REQUEST_NEW_RUN = 0; private RunCursor mCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Query the list of runs mCursor = RunManager.get(getActivity()).queryRuns(); // Create an adapter to point at this cursor RunCursorAdapter adapter = new RunCursorAdapter(getActivity(), mCursor); setListAdapter(adapter); } ... @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.run_list_options, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_new_run: Intent i = new Intent(getActivity(), RunActivity.class); startActivityForResult(i, REQUEST_NEW_RUN); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (REQUEST_NEW_RUN == requestCode) { mCursor.requery(); ((RunCursorAdapter)getListAdapter()).notifyDataSetChanged(); } } private static class RunCursorAdapter extends CursorAdapter { private RunCursor mRunCursor; The only thing new and interesting about this approach is that you use onActivityResult(…) to force the list to reload once the user returns to it after navigating elsewhere. Again, requerying the cursor as shown here, on the main thread, is not great. You will replace it with a Loader in the following chapter. 554 Working with Existing Runs Working with Existing Runs The next logical thing to do is allow the user to navigate from the list of runs to the details of a specific run. In order for this to work well, RunFragment needs some support for passing a run ID as an argument. Since you host RunFragment in RunActivity, it too needs an extra for the run ID. First, add the argument to RunFragment and a newInstance(long) method to make it easier to use. Listing 34.18 Adding a run ID argument (RunFragment.java) public class RunFragment extends Fragment { private static final String TAG = "RunFragment"; private static final String ARG_RUN_ID = "RUN_ID"; ... private TextView mStartedTextView, mLatitudeTextView, mLongitudeTextView, mAltitudeTextView, mDurationTextView; public static RunFragment newInstance(long runId) { Bundle args = new Bundle(); args.putLong(ARG_RUN_ID, runId); RunFragment rf = new RunFragment(); rf.setArguments(args); return rf; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Now, make use of the new fragment convention in RunActivity. If the intent has a RUN_ID extra, create the RunFragment using newInstance(long). If not, just use the default constructor as before. Listing 34.19 Adding a run ID extra (RunActivity.java) public class RunActivity extends SingleFragmentActivity { /** A key for passing a run ID as a long */ public static final String EXTRA_RUN_ID = "com.bignerdranch.android.runtracker.run_id"; @Override protected Fragment createFragment() { return new RunFragment(); long runId = getIntent().getLongExtra(EXTRA_RUN_ID, -1); if (runId != -1) { return RunFragment.newInstance(runId); } else { return new RunFragment(); } } } The last bit of glue to put in place is to respond to list item selection in RunListFragment by starting RunActivity with the ID of the selected run. 555 Chapter 34 Local Databases with SQLite Listing 34.20 Launching existing runs via onListItemClick(…) (RunListFragment.java) @Override public void onListItemClick(ListView l, View v, int position, long id) { // The id argument will be the Run ID; CursorAdapter gives us this for free Intent i = new Intent(getActivity(), RunActivity.class); i.putExtra(RunActivity.EXTRA_RUN_ID, id); startActivity(i); } private static class RunCursorAdapter extends CursorAdapter { There is a small bit of magic in play here. Because you named the ID column in the run table _id, CursorAdapter has detected it and passed it as the id argument to onListItemClick(…). You can therefore pass it straight along as an extra to RunActivity. How convenient! Sadly, that is where the convenience ends. Simply starting RunFragment with an ID argument is not enough to get it to display anything useful about an existing run. You need to query the database for the details of the existing run, including its last recorded location, in order to populate the user interface. Fortunately, this is quite similar to work you just did. The logical place to start is in RunDatabaseHelper, where you’ll create add a new queryRun(long) method to return a RunCursor for a single run given its ID. Listing 34.21 Querying a single run (RunDatabaseHelper.java) public RunCursor queryRun(long id) { Cursor wrapped = getReadableDatabase().query(TABLE_RUN, null, // All columns COLUMN_RUN_ID + " = ?", // Look for a run ID new String[]{ String.valueOf(id) }, // with this value null, // group by null, // order by null, // having "1"); // limit 1 row return new RunCursor(wrapped); } The many arguments to the query(…) method are made plain here. You are fetching all columns from TABLE_RUN, filtering them by the ID column, which is passed as an argument to the “where” clause using a single-element string array. You limit the query to returning only one row, wrap the result in a RunCursor, and return it. Next, you will add a getRun(long) method to RunManager that wraps the results of the queryRun(long) method that you just created and pulls a Run out of the first row, if it has one. Also add an isTrackingRun(Run) method. 556 Working with Existing Runs Listing 34.22 Implementing two methods (RunManager.java) public Run getRun(long id) { Run run = null; RunCursor cursor = mHelper.queryRun(id); cursor.moveToFirst(); // If you got a row, get a run if (!cursor.isAfterLast()) run = cursor.getRun(); cursor.close(); return run; } public boolean isTrackingRun(Run run) { return run != null && run.getId() == mCurrentRunId; } The getRun(long) method attempts to pull a Run out of the first row of the RunCursor retrieved from queryRun(long). It first tells the RunCursor to move to the first row in the results. If there are any results at all, isAfterLast() will return false, and you can safely ask for a Run for this row. Since the caller of this new method will not have access to the RunCursor, you must be careful to call close() on it before returning, so that the database can release any resources associated with the cursor from memory as soon as possible. That work complete, you can now make some changes to RunFragment to work with existing runs. Make the changes shown in Listing 34.23. Listing 34.23 Working with existing runs (RunFragment.java) public class RunFragment extends Fragment { private static final String TAG = "RunFragment"; private static final String ARG_RUN_ID = "RUN_ID"; private BroadcastReceiver mLocationReceiver = new LocationReceiver() { @Override protected void onLocationReceived(Context context, Location loc) { if (!mRunManager.isTrackingRun(mRun)) return; mLastLocation = loc; if (isVisible()) updateUI(); } @Override protected void onProviderEnabledChanged(boolean enabled) { int toastText = enabled ? R.string.gps_enabled : R.string.gps_disabled; Toast.makeText(getActivity(), toastText, Toast.LENGTH_LONG).show(); } }; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); 557 Chapter 34 Local Databases with SQLite } mRunManager = RunManager.get(getActivity());< // Check for a Run ID as an argument, and find the run Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { mRun = mRunManager.getRun(runId); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_run, container, false); ... mStartButton = (Button)view.findViewById(R.id.run_startButton); mStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRun = mRunManager.startNewRun(); if (mRun == null) { mRun = mRunManager.startNewRun(); } else { mRunManager.startTrackingRun(mRun); } updateUI(); } }); ... } return view; ... private void updateUI() { boolean started = mRunManager.isTrackingRun(); boolean trackingThisRun = mRunManager.isTrackingRun(mRun); if (mRun != null) mStartedTextView.setText(mRun.getStartDate().toString()); int durationSeconds = 0; if (mRun != null && mLastLocation != null) { durationSeconds = mRun.getDurationSeconds(mLastLocation.getTime()); mLatitudeTextView.setText(Double.toString(mLastLocation.getLatitude())); mLongitudeTextView.setText(Double.toString(mLastLocation.getLongitude())); mAltitudeTextView.setText(Double.toString(mLastLocation.getAltitude())); } mDurationTextView.setText(Run.formatDuration(durationSeconds)); } 558 } mStartButton.setEnabled(!started); mStopButton.setEnabled(started); mStopButton.setEnabled(started && trackingThisRun); Working with Existing Runs As a last kindness to your humble user, you can make RunFragment load the last location for the current run from the database. This will be very similar to loading the Run, but you will create a new LocationCursor to work with Location objects along the way. Begin by adding a method to query the last location for a run and the LocationCursor inner class in RunDatabaseHelper. Listing 34.24 Querying the last location for a run (RunDatabaseHelper.java) public LocationCursor queryLastLocationForRun(long runId) { Cursor wrapped = getReadableDatabase().query(TABLE_LOCATION, null, // All columns COLUMN_LOCATION_RUN_ID + " = ?", // limit to the given run new String[]{ String.valueOf(runId) }, null, // group by null, // having COLUMN_LOCATION_TIMESTAMP + " desc", // order by latest first "1"); // limit 1 return new LocationCursor(wrapped); } // ... After RunCursor ... public static class LocationCursor extends CursorWrapper { public LocationCursor(Cursor c) { super(c); } } public Location getLocation() { if (isBeforeFirst() || isAfterLast()) return null; // First get the provider out so you can use the constructor String provider = getString(getColumnIndex(COLUMN_LOCATION_PROVIDER)); Location loc = new Location(provider); // Populate the remaining properties loc.setLongitude(getDouble(getColumnIndex(COLUMN_LOCATION_LONGITUDE))); loc.setLatitude(getDouble(getColumnIndex(COLUMN_LOCATION_LATITUDE))); loc.setAltitude(getDouble(getColumnIndex(COLUMN_LOCATION_ALTITUDE))); loc.setTime(getLong(getColumnIndex(COLUMN_LOCATION_TIMESTAMP))); return loc; } serves the same purpose as RunCursor, except that it wraps a cursor intended to return rows from the location table and converts their various fields into properties on the Location object. One subtlety of this implementation is that Location’s constructor requires the provider name, so you first pull that out of the current row before setting up the rest of the properties. LocationCursor The queryLastLocationForRun(long) method is very similar to queryRun(long), except that it looks for the latest single location associated with a given run and wraps the result in a LocationCursor. Just as with queryRun(long), you should create a method in RunManager to wrap it and return a Location from the one row in the cursor. 559 Chapter 34 Local Databases with SQLite Listing 34.25 Getting the last location for a run (RunManager.java) public Location getLastLocationForRun(long runId) { Location location = null; LocationCursor cursor = mHelper.queryLastLocationForRun(runId); cursor.moveToFirst(); // If you got a row, get a location if (!cursor.isAfterLast()) location = cursor.getLocation(); cursor.close(); return location; } Now you can use this new method in RunFragment to fetch the last location for the current run when the fragment is created. Listing 34.26 Getting the last location for the current run (RunFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mRunManager = RunManager.get(getActivity()); } // Check for a Run ID as an argument, and find the run Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { mRun = mRunManager.getRun(runId); mLastLocation = mRunManager.getLastLocationForRun(runId); } } Now, after much ado, you should have a RunTracker that is capable of creating and tracking as many runs as your device’s disk (and battery) can withstand and displaying them to the user in a logical fashion. Happy tracking! Challenge: Identifying the Current Run As implemented, the only way to identify which run is being tracked is to manually visit it from the list and see that the start and stop buttons are enabled appropriately. It would be great if the user had an easier way to access the current run. For a simple challenge, give the list row for the current run a different treatment in the UI, like an icon or color change. For a harder challenge, use an ongoing notification to let the user know that you are tracking them, and launch the RunActivity when it is pressed. 560 35 Loading Asynchronous Data With Loaders In Chapter 34 you implemented data storage in RunTracker using SQLite and used Cursors on the main thread of the application. This was a cop-out, however: it’s a best practice to keep database work off of the main thread as much as possible. In this chapter, you will use Loaders to pull data for runs and locations from the database on a background thread. The loader API was introduced in Android 3.0 (Honeycomb) and is also available in the support library, so there is no reason not to use them in a modern application. Loaders and the LoaderManager A loader is designed to load some kind of data (an object) from some source. The source could be disk, a database, a ContentProvider, the network, or another process. The loader will do the job of fetching the data without blocking the main thread and delivering the results to whomever is interested. There are three built-in types of loaders: Loader, AsyncTaskLoader, and CursorLoader (Figure 35.1). Loader is the base class, and not very useful on its own. It defines the API that the LoaderManager uses to communicate with all loaders. is an abstract Loader that uses an AsyncTask to move the work of loading data to another thread. Almost all useful loader classes you create will be a subclass of AsyncTaskLoader. AsyncTaskLoader Finally, CursorLoader extends AsyncTaskLoader to load a Cursor from a ContentProvider via the ContentResolver. Unfortunately for RunTracker, there is no way to use CursorLoader with cursors that come from a SQLiteDatabase. 561 Chapter 35 Loading Asynchronous Data With Loaders Figure 35.1 The Loader class hierarchy All communication with loaders is handled by the LoaderManager. This class is responsible for starting, stopping, and maintaining the lifecycle of any Loaders associated with your component. Within a Fragment or Activity, you can use the getLoaderManager() method to return an implementation to work with. Use the initLoader(int, Bundle, LoaderCallbacks) method to trigger the initialization of a Loader. The first argument is an integer identifier for the loader, the second is a Bundle of arguments (which can be null), and the final argument is an implementation of the LoaderCallbacks interface. As you will see in the following sections, there are multiple ways to implement LoaderCallbacks, but the most common is to have your Fragment implement it directly. You can use the restartLoader(int, Bundle, LoaderCallbacks ) method to force an existing loader to restart. This is commonly used to reload data that you know (or suspect) is stale. The LoaderCallbacks interface has three methods: onCreateLoader(…), onLoadFinished(…) and onLoaderReset(…). You will look at each of these in more detail as you implement them in RunTracker. Why would you use a loader instead of, say, an AsyncTask directly? Well, the most compelling reason to do so is that the LoaderManager will keep your component’s loaders alive, along with their data, between configuration changes like rotation. If you use an AsyncTask to load data, you are responsible for managing its lifecycle during configuration changes and stashing its data somewhere that lives through them. Often, this is simplified by using setRetainInstance(true) on a Fragment and storing the data there, but there are still situations where you have to intervene and code you have to write in order to ensure that everything happens correctly. Loaders are designed to take some (but not all) of this pain off your hands. If, after a configuration change, you initialize a loader that has already finished loading its data, it can deliver that data immediately rather than trying to fetch it again. This works whether your fragment is retained or not, 562 Using Loaders in RunTracker which can make your life easier since you don’t have to consider the lifecycle complications that retained fragments can introduce. Using Loaders in RunTracker RunTracker currently loads three chunks of data: the list of runs (a RunCursor), an individual Run, and that run’s latest Location. Each of these comes from the SQLite database, and should therefore be moved to a Loader to create a smooth user experience. In the following sections, you will create two abstract subclasses of AsyncTaskLoader. The first, SQLiteCursorLoader, is a simplified version of the framework’s CursorLoader that will work with a Cursor coming from any source. The second is DataLoader , which is capable of loading any kind of data and simplifies the use of AsyncTaskLoader for subclasses. Loading the List of Runs The current implementation of RunListFragment asks the RunManager directly for the RunCursor representing the list of runs in onCreate(Bundle). In this section, you will introduce a loader to indirectly execute this query on another thread. The RunListFragment will tell the LoaderManager to start (and restart) the loader and implement the LoaderCallbacks to know when the data is ready. To simplify the code within the RunListFragment (and elsewhere, later), first create an abstract AsyncTaskLoader subclass called SQLiteCursorLoader as in Listing 35.1. This class mimics most of the code in CursorLoader, without the requirement of using a ContentProvider. Listing 35.1 A loader for SQLite cursors (SQLiteCursorLoader.java) public abstract class SQLiteCursorLoader extends AsyncTaskLoader { private Cursor mCursor; public SQLiteCursorLoader(Context context) { super(context); } protected abstract Cursor loadCursor(); @Override public Cursor loadInBackground() { Cursor cursor = loadCursor(); if (cursor != null) { // Ensure that the content window is filled cursor.getCount(); } return cursor; } @Override public void deliverResult(Cursor data) { Cursor oldCursor = mCursor; mCursor = data; 563 Chapter 35 Loading Asynchronous Data With Loaders if (isStarted()) { super.deliverResult(data); } } if (oldCursor != null && oldCursor != data && !oldCursor.isClosed()) { oldCursor.close(); } @Override protected void onStartLoading() { if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } @Override public void onCanceled(Cursor cursor) { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); } if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } SQLiteCursorLoader implements the AsyncTaskLoader API to efficiently load and hold a Cursor in the mCursor instance variable. The loadInBackground() method calls the abstract loadCursor() method to get the Cursor and calls the getCount() method on the cursor to ensure that the data is available in memory once it is passed over to the main thread. The deliverResult(Cursor) method takes care of two things. If the loader is started (which means the data can be delivered), the superclass implementation of deliverResult(…) is called. If the old cursor is no longer needed, it is closed to free up its resources. Because an existing cursor may be cached and redelivered, it is important to make sure that the old cursor and the new cursor are not the same before the old cursor is closed. 564 Loading the List of Runs The remaining method implementations are not critical to understand for the purposes of RunTracker, but you can find more details in the API documentation for AsyncTaskLoader. With this base class implemented, you can now implement a very simple subclass, RunListCursorLoader, in RunListFragment as an inner class. Listing 35.2 Implementing RunListCursorLoader (RunListFragment.java) @Override public void onListItemClick(ListView l, View v, int position, long id) { // The id argument will be the Run ID; CursorAdapter gives us this for free Intent i = new Intent(getActivity(), RunActivity.class); i.putExtra(RunActivity.EXTRA_RUN_ID, id); startActivity(i); } private static class RunListCursorLoader extends SQLiteCursorLoader { public RunListCursorLoader(Context context) { super(context); } @Override protected Cursor loadCursor() { // Query the list of runs return RunManager.get(getContext()).queryRuns(); } } private static class RunCursorAdapter extends CursorAdapter { Now you can update the RunListFragment to implement the LoaderCallbacks interface for a Cursor. Add the methods below and update the class declaration to declare that it implements the callbacks. 565 Chapter 35 Loading Asynchronous Data With Loaders Listing 35.3 Implementing LoaderCallbacks (RunListFragment.java) public class RunListFragment extends ListFragment implements LoaderCallbacks { ... @Override public Loader onCreateLoader(int id, Bundle args) { // You only ever load the runs, so assume this is the case return new RunListCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { // Create an adapter to point at this cursor RunCursorAdapter adapter = new RunCursorAdapter(getActivity(), (RunCursor)cursor); setListAdapter(adapter); } @Override public void onLoaderReset(Loader loader) { // Stop using the cursor (via the adapter) setListAdapter(null); } The onCreateLoader(int, Bundle) method is called by the LoaderManager when it needs you to create the loader. The id argument is useful if you have more than one loader of the same type and you need to distinguish them, and the Bundle holds any arguments that were passed in. This implementation does not use either argument, and simply creates a new RunListCursorLoader pointing at the current Activity for context. The onLoadFinished(Loader , Cursor) method will be called on the main thread once the data has been loaded in the background. In this version, you reset the adapter on the ListView to a RunCursorAdapter pointing at the new cursor. Finally, the onLoaderReset(Loader ) method will be called in the event that the data is no longer available. To be on the safe side, stop using the cursor by setting the list adapter to null. Now that you have support for the callbacks in place, you can tell the LoaderManager to do its thing. You can also remove the mCursor instance variable and the onDestroy() method that cleaned it up. 566 Loading a Single Run Listing 35.4 Using the Loader (RunListFragment.java) public class RunListFragment extends ListFragment implements LoaderCallbacks { private static final int REQUEST_NEW_RUN = 0; private RunCursor mCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Query the list of runs mCursor = RunManager.get(getActivity()).queryRuns(); // Create an adapter to point at this cursor RunCursorAdapter adapter = new RunCursorAdapter(getActivity(), mCursor); setListAdapter(adapter); // Initialize the loader to load the list of runs getLoaderManager().initLoader(0, null, this); } ... @Override public void onDestroy() { mCursor.close(); super.onDestroy(); } ... @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (REQUEST_NEW_RUN == requestCode) { mCursor.requery(); ((RunCursorAdapter)getListAdapter()).notifyDataSetChanged(); // Restart the loader to get any new run available getLoaderManager().restartLoader(0, null, this); } } With these changes done, run the app and observe that the data in the list is populated just as it was before. If you are observant, you may see that the list briefly shows a spinning progress view while the data is being loaded in the background. ListFragment provides this functionality for you automatically if the adapter is set to null. Loading a Single Run The new SQLiteCursorLoader class is fine for loading data that you want to remain in a cursor (such as a list of things), but in the RunFragment, you are loading two individual objects and the cursor is hidden by the RunManager. To best handle loading arbitrary data, you need a more generic loader. In this section, you will create a new DataLoader class that is a subclass of AsyncTaskLoader. DataLoader will take care of some simple tasks that any subclass of AsyncTaskLoader should, leaving only the implementation of loadInBackground() to its own subclasses. 567 Chapter 35 Loading Asynchronous Data With Loaders Create DataLoader using the code in Listing 35.5. Listing 35.5 A simple loader for data (DataLoader.java) public abstract class DataLoader extends AsyncTaskLoader { private D mData; public DataLoader(Context context) { super(context); } @Override protected void onStartLoading() { if (mData != null) { deliverResult(mData); } else { forceLoad(); } } @Override public void deliverResult(D data) { mData = data; if (isStarted()) super.deliverResult(data); } } DataLoader uses a generic type D to hold an instance of whatever data it is loading. In onStartLoading() it checks for the presence of this data, and if it is available, delivers Otherwise, it calls the superclass’s forceLoad() method to go and fetch it. it immediately. The deliverResult(D) method is implemented to stash away the new data object and, if the loader is started, call the superclass implementation to make the delivery. To make use of this new class, implement RunLoader, a subclass, for use in RunFragment. Listing 35.6 A loader for a run (RunLoader.java) public class RunLoader extends DataLoader { private long mRunId; public RunLoader(Context context, long runId) { super(context); mRunId = runId; } } @Override public Run loadInBackground() { return RunManager.get(getContext()).getRun(mRunId); } The RunLoader constructor expects a Context (an Activity) and a long representing the ID of the run to load. The loadInBackground() method asks the singleton RunManager for a run with that ID and returns it. 568 Loading a Single Run With this in place, you can now use RunLoader in RunFragment instead of talking directly with the RunManager on the main thread. There is one difference, however, in the implementation you use here. Since RunFragment loads two different types of data, a run and a location, the design of LoaderCallbacks coupled with limitations of Java generics prevents you from implementing the interface directly as methods within RunFragment. Instead, you can work around the limitations by creating inner classes that implement LoaderCallbacks for Run and Location separately, and pass an instance of each of them to a call to LoaderManager’s initLoader(…) method. To make the integration, first add an inner class RunLoaderCallbacks to RunFragment. Listing 35.7 RunLoaderCallbacks (RunFragment.java) private class RunLoaderCallbacks implements LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new RunLoader(getActivity(), args.getLong(ARG_RUN_ID)); } @Override public void onLoadFinished(Loader loader, Run run) { mRun = run; updateUI(); } } @Override public void onLoaderReset(Loader loader) { // Do nothing } In onCreateLoader(int, Bundle), a new RunLoader is returned pointing at the fragment’s current activity and the run ID pulled from the arguments bundle. You will pass this arguments bundle along in onCreate(Bundle). The implementation of onLoadFinished(…) stashes away the loaded run in the fragment’s mRun instance variable and calls the updateUI() method so that the UI will reflect the updated data. onLoaderReset(…) is not needed in this case since the Run instance is completely in memory. Next, use the new callbacks implementation with LoaderManager in RunFragment’s onCreate(Bundle) method. You will also add a constant called LOAD_RUN to use as the ID for the loader, which distinguishes it within the LoaderManager’s collection of loaders for RunFragment. 569 Chapter 35 Loading Asynchronous Data With Loaders Listing 35.8 Loading the run (RunFragment.java) public class RunFragment private static final private static final private static final extends Fragment { String TAG = "RunFragment"; String ARG_RUN_ID = "RUN_ID"; int LOAD_RUN = 0; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mRunManager = RunManager.get(getActivity()); } // Check for a Run ID as an argument, and find the run Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { mRun = mRunManager.getRun(runId); LoaderManager lm = getLoaderManager(); lm.initLoader(LOAD_RUN, args, new RunLoaderCallbacks()); mLastLocation = mRunManager.getLastLocationForRun(runId); } } Now you can run RunTracker again and see that it works just as it did before, only now the run data is loaded on another thread. If you are very quick (or your emulator is very slow), you might see the UI initially populate without the run date and then update to include it. Loading the Last Location for a Run For your last trick, you need to get the loading of the last location off of the main thread. This work will be almost identical to what you just did for loading the run, except that you will work with the run’s last location as the data. First, create the LastLocationLoader class to do the work. Listing 35.9 LastLocationLoader (LastLocationLoader.java) public class LastLocationLoader extends DataLoader { private long mRunId; public LastLocationLoader(Context context, long runId) { super(context); mRunId = runId; } @Override public Location loadInBackground() { return RunManager.get(getContext()).getLastLocationForRun(mRunId); } } This class is almost exactly like RunLoader, except that it calls RunManager’s getLastLocationForRun(long) method with the run ID. 570 Loading the Last Location for a Run Next, implement the LocationLoaderCallbacks inner class in RunFragment. Listing 35.10 LocationLoaderCallbacks (RunFragment.java) private class LocationLoaderCallbacks implements LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new LastLocationLoader(getActivity(), args.getLong(ARG_RUN_ID)); } @Override public void onLoadFinished(Loader loader, Location location) { mLastLocation = location; updateUI(); } } @Override public void onLoaderReset(Loader loader) { // Do nothing } Again, this class is almost just like RunLoaderCallbacks, except that it updates the mLastLocation instance variable before refreshing the UI. All that remains now is to use the new loader instead of a direct call to RunManager in onCreate(Bundle), using a new loader ID LOAD_LOCATION. Listing 35.11 Loading the last location (RunFragment.java) public class RunFragment private static final private static final private static final private static final ... extends Fragment { String TAG = "RunFragment"; String ARG_RUN_ID = "RUN_ID"; int LOAD_RUN = 0; int LOAD_LOCATION = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mRunManager = RunManager.get(getActivity()); } // Check for a Run ID as an argument, and find the run Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { LoaderManager lm = getLoaderManager(); lm.initLoader(LOAD_RUN, args, new RunLoaderCallbacks()); mLastLocation = mRunManager.getLastLocationForRun(runId); lm.initLoader(LOAD_LOCATION, args, new LocationLoaderCallbacks()); } } Now RunTracker is capable of loading all of its important data on a background thread, thanks to loaders. Run it now and verify that everything works as before. 571 This page intentionally left blank 36 Using Maps The next logical step for RunTracker is to show the user a map of his or her travels. Using the new Google Maps API (version 2), this is an easy task. In this chapter, you will create a new RunMapFragment class to display a map showing the track of the user’s run and interactive markers that indicate the start and end of the journey. Before starting with the fun stuff, however, you must set up your project to use the Maps API. Adding the Maps API to RunTracker The Maps API (version 2) is provided by the Google Play services SDK, and has several requirements for both your development environment and your application. Use a real device to test maps The Google Play services SDK (and thus the Maps API) requires a real device running at least Android 2.2 with the Google Play store installed. Running on an emulator is not supported. Install and use the Google Play services SDK To make the Maps API available to your project, you first need to install the Google Play services SDK add-on and configure its library project to work with your application. The following steps should get you there, but the latest details should always be available at http://developer.android.com/ google/play-services/ 1. From the Android SDK Manager, install the Google�Play�services add-on from the Extras section. This will be installed within your Android SDK directory under extras/google/ google_play_services. 2. In Eclipse, import a copy of the library project into your workspace using File → Import... → Existing�Android�Code�Into�Workspace. The library project is within the Google Play services extra directory under libproject/google-play-services_lib. Be sure to choose Copy�projects into�workspace from the import wizard so that you are working with your own copy of the project. 3. Open the RunTracker project’s Properties window and add a reference to the library project under Android, Library. Click the Add... button and select the google-play-services_lib project. 573 Chapter 36 Using Maps Obtain a Google Maps API key In order to use the Maps API, you need to create an API key for your application. This process requires several steps, and Google has better documentation than we can provide here. Check out https:// developers.google.com/maps/documentation/android/start and follow the instructions there to obtain your key. Update RunTracker’s manifest In order for the Google Play services and Maps API to work, you need to add several permissions and requirements to your application manifest, in addition to the API key you just obtained. Add the highlighted XML to the RunTracker manifest now. Take care to ensure that the permission that ends in MAPS_RECEIVE begins with RunTracker’s package name. While you’re there, go ahead and add an activity entry for the forthcoming RunMapActivity. Listing 36.1 Maps API requirements (AndroidManifest.xml) Showing the User’s Location on a Map With the requirements behind you, now it is time to enjoy the fruits of your labor and show the user a map. The Maps API includes MapFragment and SupportMapFragment that you can subclass to conveniently handle the work of setting up the MapView and its associated model object, GoogleMap. Create RunMapFragment, a subclass of SupportMapFragment, in a new file using the code below. Listing 36.2 Basic RunMapFragment (RunMapFragment.java) public class RunMapFragment extends SupportMapFragment { private static final String ARG_RUN_ID = "RUN_ID"; private GoogleMap mGoogleMap; public static RunMapFragment newInstance(long runId) { Bundle args = new Bundle(); args.putLong(ARG_RUN_ID, runId); RunMapFragment rf = new RunMapFragment(); rf.setArguments(args); return rf; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = super.onCreateView(inflater, parent, savedInstanceState); // Stash a reference to the GoogleMap mGoogleMap = getMap(); // Show the user's location mGoogleMap.setMyLocationEnabled(true); } return v; } 575 Chapter 36 Using Maps RunMapFragment’s newInstance(long) method takes a run ID and sets up the arguments to a new instance of the fragment, just as the code in RunFragment does. You will use this argument shortly when fetching a list of locations. The implementation of onCreateView(…) calls the superclass implementation to get and return a view, but it relies on the fact that doing so will initialize the fragment’s GoogleMap instance. GoogleMap is the model object that is tied to the MapView and you will use it to configure the various additions to the map. In this initial version of the fragment, simply call setMyLocationEnabled(boolean) to allow the user to see and navigate to his or her location on the map. To host the new RunMapFragment, create a simple RunMapActivity class using the following code. Remember that you already added the reference to this class in the manifest earlier. Listing 36.3 An activity to host a fragment (RunMapActivity.java) public class RunMapActivity extends SingleFragmentActivity { /** A key for passing a run ID as a long */ public static final String EXTRA_RUN_ID = "com.bignerdranch.android.runtracker.run_id"; @Override protected Fragment createFragment() { long runId = getIntent().getLongExtra(EXTRA_RUN_ID, -1); if (runId != -1) { return RunMapFragment.newInstance(runId); } else { return new RunMapFragment(); } } } Now you will need code to launch RunMapActivity from RunFragment when a run is available. To do this, add a button to the layout that will trigger the map. But first, as usual, add a few new strings to support this button and the work for the rest of the chapter. Listing 36.4 Strings for the UI (res/values/strings.xml) 574 Showing the User’s Location on a Map New Run Map Run Start Run started at %s Run Finish Run finished at %s Now update RunFragment’s layout to include the new Map button. 576 Showing the User’s Location on a Map Listing 36.5 Adding a Map button (fragment_run.xml){ private static final String ARG_RUN_ID = "RUN_ID"; private static final int LOAD_LOCATIONS = 0; private GoogleMap mGoogleMap; private LocationCursor mLocationCursor; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } // Check for a Run ID as an argument, and find the run Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { LoaderManager lm = getLoaderManager(); lm.initLoader(LOAD_LOCATIONS, args, this); } } ... @Override public Loader onCreateLoader(int id, Bundle args) { long runId = args.getLong(ARG_RUN_ID, -1); return new LocationListCursorLoader(getActivity(), runId); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { mLocationCursor = (LocationCursor)cursor; } @Override public void onLoaderReset(Loader loader) { // Stop using the data mLocationCursor.close(); mLocationCursor = null; } } can now stash a LocationCursor to hold on to the list of locations. It will use this to populate the map with the route. RunMapFragment The implementation of onLoaderReset(Loader ) takes care of closing and dereferencing the cursor when it is no longer available. Under normal circumstances, this method will be called when the LoaderManager is shutting the loader down after the user navigates away from the map fragment. 580 Displaying a Run’s Path This will keep things nice and tidy, but one thing it won’t do is close the cursor during rotation. This is exactly what you want. Now you need to get to the meat of the chapter: displaying some things on the GoogleMap. Add an updateUI() method to set up the run track, and call that method in onLoadFinished(…). Listing 36.11 Putting the run on the map (RunMapFragment.java) private void updateUI() { if (mGoogleMap == null || mLocationCursor == null) return; } // Set up an overlay on the map for this run's locations // Create a polyline with all of the points PolylineOptions line = new PolylineOptions(); // Also create a LatLngBounds so you can zoom to fit LatLngBounds.Builder latLngBuilder = new LatLngBounds.Builder(); // Iterate over the locations mLocationCursor.moveToFirst(); while (!mLocationCursor.isAfterLast()) { Location loc = mLocationCursor.getLocation(); LatLng latLng = new LatLng(loc.getLatitude(), loc.getLongitude()); line.add(latLng); latLngBuilder.include(latLng); mLocationCursor.moveToNext(); } // Add the polyline to the map mGoogleMap.addPolyline(line); // Make the map zoom to show the track, with some padding // Use the size of the current display in pixels as a bounding box Display display = getActivity().getWindowManager().getDefaultDisplay(); // Construct a movement instruction for the map camera LatLngBounds latLngBounds = latLngBuilder.build(); CameraUpdate movement = CameraUpdateFactory.newLatLngBounds(latLngBounds, display.getWidth(), display.getHeight(), 15); mGoogleMap.moveCamera(movement); @Override public Loader onCreateLoader(int id, Bundle args) { long runId = args.getLong(ARG_RUN_ID, -1); return new LocationListCursorLoader(getActivity(), runId); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { mLocationCursor = (LocationCursor)cursor; updateUI(); } This new method uses several bits of the Maps API. First, it creates an instance of PolylineOptions that will be used to build up the line to draw on the map and an instance of LatLngBounds.Builder to create a bounding box to zoom the map to when everything is collected. Next, it iterates over the LocationCursor, and for each Location creates a LatLng using the coordinates. It adds this LatLng to the PolylineOptions and includes it in the LatLngBounds before moving to the next row in the cursor. 581 Chapter 36 Using Maps After consuming all of the locations, it calls addPolyline(PolylineOptions) on the GoogleMap, adding the line to the map. The next task is to make the map zoom to show the entire line. Moving around the map is the job of the “camera,” and the camera is adjusted using commands packaged in instances of CameraUpdate and passed to moveCamera(CameraUpdate). Creating an instance to move the camera to the bounds of the line you just added is the job of the CameraUpdateFactory’s newLatLngBounds(LatLngBounds, int, int, int) method. Here you pass the dimensions of the current screen as an approximation of the size of the map in pixels, and specify a few pixels of padding to make things fit nicely. There is a simpler version of the method, newLatLngBounds(LatLngBounds, int), but that version will throw an IllegalStateException if you call it before the MapView has completed sizing itself via the layout process. Since you cannot guarantee that has happened by the time updateUI() is called, you have to make your best guess using the display dimensions. With these changes in place, you can run RunTracker again and see a beautiful black line tracing your route. Perhaps now would be a good time to investigate the rest of the PolylineOptions class and try to make it prettier. Adding Markers for Run Start and Finish Now that you have everything else in place, it is relatively trivial to add markers to the map that show the start and finish locations of the run. You can also add some text along with those markers that will show up in an info window when you touch them. Add the code highlighted below to updateUI(). Listing 36.12 Start and end markers with info (RunMapFragment.java) private void updateUI() { if (mGoogleMap == null || mLocationCursor == null) return; // Set up an overlay on the map for this run's locations // Create a polyline with all of the points PolylineOptions line = new PolylineOptions(); // Also create a LatLngBounds so you can zoom to fit LatLngBounds.Builder latLngBuilder = new LatLngBounds.Builder(); // Iterate over the locations mLocationCursor.moveToFirst(); while (!mLocationCursor.isAfterLast()) { Location loc = mLocationCursor.getLocation(); LatLng latLng = new LatLng(loc.getLatitude(), loc.getLongitude()); Resources r = getResources(); // If this is the first location, add a marker for it if (mLocationCursor.isFirst()) { String startDate = new Date(loc.getTime()).toString(); MarkerOptions startMarkerOptions = new MarkerOptions() .position(latLng) .title(r.getString(R.string.run_start)) .snippet(r.getString(R.string.run_started_at_format, startDate)); mGoogleMap.addMarker(startMarkerOptions); 582 Challenge: Live Updates } else if (mLocationCursor.isLast()) { // If this is the last location, and not also the first, add a marker String endDate = new Date(loc.getTime()).toString(); MarkerOptions finishMarkerOptions = new MarkerOptions() .position(latLng) .title(r.getString(R.string.run_finish)) .snippet(r.getString(R.string.run_finished_at_format, endDate)); mGoogleMap.addMarker(finishMarkerOptions); } line.add(latLng); latLngBuilder.include(latLng); mLocationCursor.moveToNext(); } // Add the polyline to the map mGoogleMap.addPolyline(line); For each of the first and last locations, this code creates an instance of MarkerOptions to hold the position, title, and snippet text. The title and snippet text are displayed in a simple info window when the user presses the marker. In this code, you are relying on the default marker icon to be used, but you could use the icon(BitmapDescriptor) method and a BitmapDescriptorFactory to create a marker of a different color or even with your own custom graphics. There are many standard colors (or hues, as they are called) to choose from. Run RunTracker and test it out on your next trip. Happy travels! Challenge: Live Updates At this point, RunMapFragment is capable of displaying a map of the run’s locations frozen in time, as they were when the user navigated there. A proper run-tracking app should show updates to the track in a live fashion. Using a LocationReceiver subclass in RunMapFragment, respond to new locations by redrawing the map adornments. Remember to clear any previous track overlay and markers before adding new ones. 583 This page intentionally left blank 37 Afterword Congratulations! You are at the end of this guide. Not everyone has the discipline to do what you have done, to learn what you have learned. Take a quick moment and give yourself a pat on the back. This hard work has paid off: you are now an Android developer. The Final Challenge We have one last challenge for you: become a good Android developer. Good developers are each good in their own way, so you must find your own path from here on out. Where might you start, then? Here are some places we recommend: Write code. Now. You will quickly forget what you have learned here if you do not apply it. Contribute to a project, or write a simple application of your own. Whatever you do, waste no time: write code. Learn. You have learned a little bit about a lot of things in this book. Did any of them spark your imagination? Write some code to play around with your favorite thing. Find and read more documentation about it, or an entire book if there is one. Meet people. Lots of top-notch Android developers hang out in #android-dev on irc.freenode.net. Android Developer Office Hours (https://plus.google.com/+AndroidDevelopers/posts) are good to stay connected with the Android development team and other interested developers. Local meetups can also be a good place to meet like-minded developers. Explore the open source community. Android development is exploding on http://www.github.com. When you find a cool library, see what other projects its contributors are committing to. Share your own code, too – you never know who will find it useful or interesting. Shameless Plugs You can find both of us on Twitter. Bill is @billjings, and Brian is @lyricsboy. If you enjoyed this book, check out other Big Nerd Ranch Guides at http://www.bignerdranch.com/ books. We also have a broad selection of week-long courses for developers, where we make it easy to learn this amount of stuff in only a week of time. And of course, if you just need someone to write great code, we do contract programming, too. For more, go to our website at http:// www.bignerdranch.com. Thank You Without readers like you, our work would not exist. Thank you for buying and reading our book. 585 This page intentionally left blank Index Symbols 9-patch images, 409, 410 @+id, 18, 187 @Override, 56 @SuppressLint("HandlerLeak"), 444 @SuppressWarnings("deprecation"), 306 @SuppressWarnings("unchecked"), 445 @TargetApi annotation, 119 A aapt (Android Asset Packing tool), 27 AbsListView.MultiChoiceModeListener interface, 289 action bar action view in, 463 android:showAsAction, 256 compatibility and, 294 contextual (see contextual action bar) features, 253 hiding, 315 options menu, 253 overflow menu, 256 ActionBar onCreateOptionsMenu(…), 257 setDisplayHomeAsUpEnabled(…), 263 ActionBarSherlock library, 294-298 ActionMode, 289, 293 ActionMode.Callback interface, 289-291, 293 activities (see also Activity, fragments) abstract fragment-hosting activity, 172 adding to project, 89-112 as controller, 36 back stack of, 110-112, 380 base, 380 child, 90, 104 defined, 2 fragment transactions and, 365 handling configuration changes in, 510 hosting fragments, 128, 134-137, 232, 233 label (display name), 377 launcher, 109, 317 lifecycle and fragments, 146 lifecycle diagram, 67 lifecycle of, 53, 61, 67, 68 managing fragments, 365-373 metadata for, 265 overriding methods, 54 passing data between, 101-109 record, 67 rotation and, 61-65 running from command line, 317 searchable, 455, 458 starting from fragment, 191 starting in current task, 381 starting in new task, 382 states of, 53, 67 tasks and, 380 UI flexibility and, 126 Activity as Context subclass, 23 FragmentActivity, 131 getIntent(), 103, 193 invalidateOptionsMenu(), 477 lifecycle methods, 53-61 onActivityResult(…), 106 onCreate(…), 16, 53, 55 onDestroy(), 53 onPause(), 53 onResume(), 53, 197 onSaveInstanceState(…), 65-67, 241, 243 onSearchRequested(), 453 onStart(), 53 onStop(), 53 setContentView(…), 16 setResult(…), 106 SingleFragmentActivity, 172, 174-176, 361 startActivity(…), 99 startActivityForResult(…), 104 startSearch(…), 465 activity record, 67 ActivityInfo, 379 ActivityManager back stack, 110, 112 starting activities, 99, 101, 102, 106, 107 ActivityNotFoundException, 101 Adapter interface, 179 adapters adapter views and, 179 creating custom, 187 defined, 179 implementing, 180 587 Index AdapterView (see also ListView, GridView) setEmptyView(…), 270 adb (Android Debug Bridge), 44 resetting, 45, 75 running activities with, 317, 318 add(…) (FragmentTransaction), 144 addFlags(…) (Intent), 382 ADT (Android Developer Tools) installing, xx updating, xxii AlarmManager, 473-475 AlertDialog, 211-215 AlertDialog.Builder, 213 constructor, 213 create(), 214 setPositiveButton(…), 213 setTitle(…), 213 setView(…), 215 alias resources, 363-365 am (activity manager) tool, 318 ancestral navigation, 262-266 Android Asset Packing tool, 27 Android Debug Bridge (see adb (Android Debug Bridge)) Android developer documentation, 120-122 Android firmware versions, 113 Android Lint @SuppressLint("HandlerLeak"), 444 @TargetApi annotation, 119 as static analyzer, 85 compatibility and, 118-120 running, 86 suppressing compatibility errors, 119 Android SDK Manager, xxi Android versions (see SDK versions) Android Virtual Device Manager, 24 Android XML namespace, 12 android.os.Environment, 280 android.support.v4.app.Fragment, 231 android.text.format.DateFormat, 166 android.util.Log (see Log) android:actionViewClass, 463 android:background, 401 android:configChanges, 510 android:contentDescription, 51 android:drawable, 292 android:drawablePadding, 48 588 android:drawableRight, 48 android:focusable, 189 android:icon, 256 android:layout_gravity, 63 android:layout_height, 13 android:layout_weight, 164, 301 android:layout_width, 13, 301 android:minSdkVersion, 115, 116 android:orientation, 13 android:padding, 156 android:protectionLevel, 495 android:resource, 457 android:screenOrientation, 304 android:showAsAction, 256 android:state_activated, 292 android:state_pressed, 403 android:stretchColumns, 390 android:stretchMode, 416 android:targetSdkVersion, 115, 116 android:theme, 231 AndroidManifest.xml (see manifest) anonymous inner classes, xix, 20-22 API levels (see SDK versions) .apk file, 26, 281 app data loading, 278 saving in onPause(), 277 saving to JSON file, 274 when to save, 276 app icon, 262-264 appendQueryParameter(…) (Uri.Builder), 424 application context, 169 arguments bundle, 195, 196 ArrayAdapter , 179, 180 ArrayList, 168 AsyncTask cancel(…), 432 doInBackground(…), 419 for running on background thread, 419 vs. HandlerThread, 449 onPostExecute(…), 430 onProgressUpdate(…), 432 publishProgress(…), 432 AsyncTaskLoader, 561 AttributeSet, 516 audio playback, 227, 233-236 auto-complete, using, 23, 24 AVDs (Android Virtual Devices) Index creating, 24 for tablets, 359 B Back button, 59, 60, 380 back stack, 110 background threads dedicated, 438 updating UI from, 430 using AsyncTask for, 419, 422 beginTransaction() (FragmentTransaction), 143 Bitmap.recycle(), 337 breakpoints (see also debugging) exception, 83, 84 removing, 83 setting, 80-83 broadcast intents defined, 485 ordered, 495-499 permissions and, 493-495 registered in code, 490-492 sending, 489, 490 broadcast receivers defined, 485, 486 dynamic, 490-492 implementing, 486, 487 intent filters and, 487 location updates and, 546-548 long-running tasks and, 499 permissions and, 492-495 registering, 486 uses for, 488, 489 build errors (see also debugging) debugging, 87 missing imports, 19 build process, 26 build SDK version, 116 build target, 116 Build.VERSION.SDK_INT, 119 Bundle for fragment arguments, 195, 196 in onCreate(…), 66 in onSaveInstanceState(…), 65 putCharSequence(…);, 195 putInt(…);, 195 putSerializable(…), Button 195 adding icon to, 48 example, 9 v. ImageButton, 50 inheritance, 50 button adding ID, 18 buttons, 50 C caching, 450 Calendar, 219 Callbacks interface, 365-373 camera accessing, 304-306 checking for, 306, 314, 315 compatibility and, 305 different types of, 305, 314 as exclusive-access resource, 300, 304 image file sizes and, 299 image handling and, 334-338 implicit intent for, 299 parameters of, 310, 324 permissions for, 303 setting picture size, 324 setting preview size, 310 as Surface client, 307-309 taking pictures, 321-323 Camera getSupportedPictureSizes(), 324 getSupportedPreviewSizes(), 310 open(…), 304 Parameters inner class, 310, 324 PictureCallback interface, 321-323 release(), 304 setPreviewDisplay(…), 308 ShutterCallback interface, 321-323 startPreview(), 308 stopPreview(), 308 takePicture(…), 321, 323 cancel(…) (AlarmManager), 474, 475 cancel(…) (AsyncTask), 432 Canvas, 522 CheckBox, 151 focusable, 189 589 Index CHOICE_MODE_MULTIPLE_MODAL, 288 choosers, creating, 352 codenames, version, 113 commands (IntentService), 467 compatibility action bar and, 294 with ActionBarSherlock library, 294-298 ActionBarSherlock library, 296 Android Lint and, 118-120 camera and, 305 context actions and, 283 for ListFragment, 170 fragments and, 131, 132 importance of, xix, 113-115 issues, 114 layout attributes and, 256 minimum SDK version and, 116 options menus and, 283 strategies, 293, 294 with support library, 131, 132 using conditional code for, 118 wrapping code for, 118-120 ComponentName, 380 components, 100 configuration changes, 61, 65, 238 configuration qualifiers defined, 62 for language, 245 listed, 247 multiple, 249, 250 precedence of, 247-250 for screen density, 46, 246 for screen orientation, 62 for screen size, 364, 373, 374 ConnectivityManager, 470 contacts getting data from, 355 permissions for, 356 container views, 136, 144 content assist, using, 23, 24 ContentProvider, 355 ContentResolver, 355 Context application-wide, 169 explicit intents and, 100 getSharedPreferences(…), for I/O, 274, 281 resource IDs and, 23 590 461 singletons and, 169 context menus compatibility and, 283 defining menu, 284 determining selected item, 287 example of, 283 implementing, 284-287 ContextMenu.ContextMenuInfo interface, 286 contextual action bar, 283, 287 (see also contextual action mode) contextual action mode callbacks, 289 defined, 287 defining menu, 284 enabling multiple selection, 288 implementing in ListView, 288-291 implementing in other views, 293 contextual actions, 253, 283 controller objects, 36 conventions class naming, 6 extra naming, 102 package naming, 3 variable naming, 19, 33 corner (drawable), 401 create() (AlertDialog.Builder), 214 create(…) (MediaPlayer), 234 createChooser(…) (Intent), 352 Cursor, 541, 548 CursorAdapter, 551 CursorLoader, 561 CursorWrapper, 549 D Dalvik Debug Monitor Service (see DDMS) Dalvik virtual machine, 384 /data/app directory, 281 /data/data directory, 273 databases, 541-554 Date, 219 DatePicker, 215 DatePickerDialog, 211 DDMS (Dalvik Debug Monitor Service), 75 DDMS perspective, 75 Debug view, 80 debugging (see also Android Lint) Index build errors, 87 crash, 76 crash on unconnected device, 77 misbehaviors, 77 online help for, 88 R, 87 running app with debugger, 80 stopping debugger, 82 DEFAULT (Intent), 383 default resources, 246 delayed execution, 472, 473 density-independent pixel (see dip) deprecation in Android, 342 using deprecated methods, 306, 342 detach(…) (FragmentTransaction), 208 Dev Tools, 70 developer documentation, 120-122 devices configuration changes and, 61 configuring language settings, 245 hardware, 24 virtual, 24, 359 Devices view, 44, 75 Dialog, 211 DialogFragment, 211 onCreateDialog(…), 213 show(…), 214 showing image in, 338 dialogs, 211-217 dip (density-independent pixel), 155 documentation, 120-122 doInBackground(…) (AsyncTask), 419 dp (density-independent pixel), 155 draw() (View), 522 draw9patch tool, 410 drawables, 399, 401 9-patch images, 409 corner, 401 documentation, 401 gradient, 401, 405 inset, 404, 405 layer-list, 404, 405 referencing, 48 state list, 291, 402, 403 stretchable, 406 drawing Canvas, 522 in onDraw(…), 522 Paint, 522 to Surface, 306-309 E Eclipse auto-complete, 23, 24 build process, 26 code style preferences, 33 content assist, 23, 24 creating new classes, 32 debugger, 80, 81 (see also debugging) editor, 7 generating getter and setter methods, 33, 34, 133 graphical layout tool, 157, 251 installing, xx organizing imports, 19, 24 package explorer, 7 panes, resizing, 57 preferences, 33 res/values directory, 14 src directory, 15 tab group, 7 views, 75 Debug view, 80 Devices view, 44, 75 Lint Warnings view, 86 moving, 7, 56 Variables view, 81 workbench window, 7 EditText, 137 emulator (see also virtual devices) rotating, 48 running on, 24 system images for, xxi for tablets, 359 errors (see also debugging) missing imports, 19 escape sequence (in string), 39 exception breakpoints, 83, 84 exceptions, 76 explicit intents creating, 100 591 Index creating at runtime, 380 purpose, 101 external storage checking for, 280 defined, 273 using, 280 extras defined, 102 fragments retrieving from activity, 193 as key-value pairs, 102 naming, 102 putting, 102, 103 retrieving, 103 structure of, 102 F file explorer, 84 file I/O, 273-280 fill_parent, 13 finalizer execution, 337 Flickr API, 422 floating context menus (see context menus) fluent interface, 143 format string, 348 Fragment from native libraries, 147 from support library, 131 getActivity(), 171, 191, 193 getArguments(…), 196 getTargetFragment(), 221 getTargetRequestCode(), 221 newInstance(…), 195 onActivityResult(…), 217 onContextItemSelected(…), 284, 286 onCreate(Bundle), 140 onCreateContextMenu(…), 284 onCreateOptionsMenu(…), 257, 258 onCreateView(…), 140 onOptionsItemSelected(…), 261 onPrepareOptionsMenu(…), 477 onSaveInstanceState(…), 140, 237 registerForContextMenu(…), 285 setArguments(…), 195 setHasOptionsMenu(…), 258 setRetainInstance(…), 237 setTargetFragment(…), 221 startActivityForResult(…), 199 592 fragment arguments, 193, 195, 196 fragment transactions, 143-145, 365, 367, 368 (see also FragmentTransaction) FragmentActivity (from support library), 131 FragmentManager adding fragments to, 142-145 fragment lifecycle and, 145-147 layout fragments and, 232 onResume(), 197 responsibilities, 142 role in rotation, 237 FragmentPagerAdapter, 208 fragments (see also fragment transactions, FragmentManager) accessing extra in activity’s intent, 193 v. activities, 126 activity lifecycle and, 146 adding in code, 136 adding to FragmentManager, 142-147 arguments of, 195, 196 as composable units, 126, 365 Callbacks interface, 365-373 compatibility and, 131, 132 container views for, 136, 361 creating, 137 creating from support library, 139 defined, 126 delegating functionality to activity, 365 handling activity functions, 171 hosting, 128, 134-137, 232, 233 hosting options, 135 implementing lifecycle methods, 139 inflating layouts for, 140 layout, 135, 232, 233 lifecycle diagram, 146 lifecycle methods, 146 lifecycle of, 134, 135, 145-147, 233 maintaining independence of, 195, 365 passing data between (same activity), 217 passing data with fragment arguments, 218 reasons for, 126, 127, 147 retaining, 237-243 rotation and, 238-240, 243 setting a result from, 326-328 setting listeners in, 141 starting activities from, 191 support library and, 131, 132 Index without support library, 147 UI flexibility and, 126 widgets and, 141 FragmentStatePagerAdapter, 204 getCount(), 204, 205 getItem(…), 204, 205 setOffscreenPageLimit(…), 206 FragmentTransaction add(…), 144 beginTransaction(), detach(…), remove(…), FrameLayout getTargetRequestCode() (Fragment), 221 getter and setter methods, generating, 33, 34, 133 getWritableDatabase() (SQLiteOpenHelper), 543 Google APIs, 117, 526 Google Maps API, 573, 574 Google Play services SDK, 573 GPS adding ACCESS_FINE_LOCATION permission, 531 adding ACCESS_MOCK_LOCATION permission, 537 adding uses-feature, 531 gradient (drawable), 401, 405 graphical layout tool, 157-165, 251 143 208 208 as container view for fragments, 136, 361 described, 63 replacing with merge, 87 GridView (see also AdapterView, ListView) for display columns, 416 setOnItemClickListener(…), 503 G Gallery, 209 gen directory, 16 getAction() (MotionEvent), 518 getActiveNetworkInfo() (ConnectivityManager), 470 getActivity() (Fragment), 171, 191, 193 getArguments(…) (Fragment), 196 getBackgroundDataSetting() (ConnectivityManager), 470 getBooleanExtra(…) (Intent), 103 getBroadcast(…) (PendingIntent), 530 getCount() (FragmentStatePagerAdapter), H Handler, 441, 445 handlers, 441-449 HandlerThread vs. AsyncTask, 204, 205 getDefaultSharedPreferences(…) (PreferenceManager), 461 getInputStream() (HttpURLConnection), 418 getIntent() (Activity), 103, 193 getItem(…) (FragmentStatePagerAdapter), 204, 205 getListAdapter() (ListFragment), 184 getLoaderManager() (LoaderManager), 562 getMenuInfo() (MenuItem), 286 getOutputStream() (HttpURLConnection), 418 getReadableDatabase() (SQLiteOpenHelper), 543 getSearchableInfo(…) (SearchView), 464 getService(…) (PendingIntent), 473 getSharedPreferences(…) (Context), 461 getTargetFragment() (Fragment), 221 449 handling downloads, 439 hardware devices, 24 hardware menu key, 256 -hdpi suffix, 46 hierarchical navigation, 262 HOME (Intent), 383 Home button, 60 home screen, 383 Honeycomb, 114 HTTP networking, 414, 417-419, 422 HttpURLConnection class, 418 getInputStream(), 418 getOutputStream(), 418 I icons, 256, 257 image handling, 334-338 ImageButton, 50 ImageView, 331 implicit intents action, 350, 376 593 Index category, 357 category, 357 category, 353 category, 351 benefits of using, 345 for camera, 299 categories, 350, 376 CATEGORY_DEFAULT, 377 data, 350 v. explicit intent, 350 parts of, 350 sending with AlarmManager, 473 type, 350 include (layout), 394 inflating layouts, 16, 140 initLoader(…) (LoaderManager), 562 ACTION_CALL ACTION_DIAL ACTION_PICK ACTION_SEND InputStream for delivering bytes, 418 read(), 418 inset (drawable), 404, 405 Intent addFlags(…), 382 constructors, 100 createChooser(…), 352 getBooleanExtra(…), 103 putExtra(…), 102, 193 setClassName(…), 380 setComponent(…), 380 intent filters BOOT_COMPLETED, 486 explained, 350 MAIN, 109 SEARCH, 457 SHOW_NOTIFICATION, 491 intent services processing commands, 468 purpose, 467 intents (see also broadcast intents, explicit intents, extras, implicit intents, Intent) communicating with, 100, 101 implicit v. explicit, 345 IntentService, 467 internal storage, 273 invalidate() (View), 521 invalidateOptionsMenu() (Activity), 477 is prefix for variable names, 33 594 J JavaScript Object Notation (see JSON) JavaScript, enabling, 507 Javascript, injecting objects, 510 JSON (JavaScript Object Notation) defined, 274 loading, 278-280 saving app data as, 274-277 serialization, 276 v. SQLite, 541 toJSON(), 275 JSONArray, 275 JSONObject, 276 L qualifier, 62 language settings, 245 launch mode for search, 456 types, 459 launcher activities, 109 LAUNCHER category, 109, 377 layer-list (drawable), 404, 405 layout attributes -land android:contentDescription, 51 android:drawable, 292 android:drawablePadding, 48 android:drawableRight, 48 android:focusable, 189 android:icon, 256 android:layout_gravity, 63 android:layout_height, 13 android:layout_weight, 164, 301 android:layout_width, 13, 301 android:orientation, 13 android:padding, 156 android:stretchColumns, 390 android:stretchMode, 416 compatibility and, 256 layout fragments, 232, 233 layout parameters (layout_), 156 LayoutInflater, 27, 141 layouts (see also graphical layout tool, layout attributes, widgets) alternative, 62-64 creating new, 91 Index defined, 2 defining in XML, 11-13 design documentation, 155 include, 394 inflating, 16, 140 landscape, 62-64 managing multiple, 165 merge, 397 naming, 6, 92 previewing, 14, 94, 251 root element, 12 testing, 251 view hierarchy and, 12 layout_weight attribute Android attribute, 164 -ldpi suffix, 46 library projects, 294 LinearLayout, 9, 12 lint (see Android Lint) Lint Warnings view, 86 list items activated state, 291 changing background, 291 customizing display of, 184 handling selection, 183, 288, 291 setting text of, 182 list-detail interfaces, 125, 201, 359-369 listeners defined, 20 as interfaces, 20 setting in fragments, 141 setting up, 20-24 ListFragment compatibility and, 170 creating, 170, 171 defined, 168 getListAdapter(), 184 ListView and, 171 onListItemClick(…), 183 setListAdapter(…), 181 support library class, 170 lists (see also adapters, ListFragment, ListView) basic mechanics of, 178-184 displaying, 167 getting item data, 179 getting the adapter, 184 setting adapter, 181 ListView (see also AdapterView) basic operations of, 178 default implementation, 171 enabling multiple selection, 288 setChoiceMode(…), 288 setEmptyView(…), 270 Loader, 561 LoaderCallbacks, 562 onCreateLoader(…), 566 onLoaderReset(…), 566 onLoadFinished(…), 566 LoaderManager, 562 loaders, 561-571 loadLabel(…) (ResolveInfo), 378 local files (see app data) localization, 245, 246 location updates (see also GPS) delivery mechanisms, 529 last known location, 536 receiving broadcast updates, 531 testing, 533 testing real device with a TestProvider, 537 testing virtual device with DDMS, 537 LocationListener, 529 LocationManager, 528-530 Log, 54 Log.d(…), 78 LogCat, 56-61 (see also logging) logging levels, 70 Log.d(…), 78 messages, 54 methods, 70 of exceptions, 78 of stack traces, 78 TAG constant, 54 long-press, simulating on emulator, 287 Looper, 439, 442 LRU (least recently used) caching strategy, 450 LRUCache, 450 M m prefix for variable names, MAIN category, 109, 377 19 595 Index main thread, 421 makeText(…) (Toast), 23 manifest (see also manifest attributes) adding camera permissions to, 303 adding GPS permissions to, 531 adding network permissions to, 419 adding service to, 468 adding uses-feature GPS to, 531 adding uses-permission INTERNET, 419 Android versions in, 115 build process and, 26 declaring Activity in, 97 defined, 97 uses-feature, 304 uses-permission, 303 uses-sdk, 115 manifest attributes android:configChanges, 510 android:protectionLevel, 495 android:resource, 457 android:screenOrientation, 304 android:theme, 231 maps adding live updates to, 583 adding markers to, 582 displaying, 575 displaying a path on, 579 master-detail interfaces, 125, 201, 359-369 match_parent, 13 -mdpi suffix, 46 MediaPlayer, 227 create(…), 234 documentation, 236 on different thread, 235 release(), 234 stop(), 234 video playback, 236 MenuItem, 261, 286 menus (see also action bar, context menus, options menus) android:actionViewClass, 463 creating XML file for, 255 defining in XML, 255 as resources, 255 search and, 463 merge (layout), 87, 397 596 Message, 441 message handlers, 441-449 message loop, 438 message queue, 438 messages, 441-449 meta-data parent activity, 265 searchable, 457 minimum required SDK, 115 minSdkVersion, 115, 116 model layer, 35 model objects, 35 motion events, handling, 518-521 MotionEvent actions, 518 class, 518 getAction(), 518 MVC (Model-View-Controller), 35-37 N namespace, Android XML, 12 navigateUpFromSameTask(…) (NavUtils), navigation types of, 262 NavUtils for ancestral navigation, 265 navigateUpFromSameTask(…), 265 network, checking availability of, 470 networking (HTTP), 414, 417-419, 422 NetworkOnMainThreadException, 421 newInstance(…) (Fragment), 195 9-patch images, 409, 410 Notification, 478 NotificationManager, 478 notifications, 478, 479 notify(…) (NotificationManager), 478 NullPointerException, 76 O onActionItemClicked(…) (ActionMode.Callback), 289 onActivityResult(…) (Activity), 106 onActivityResult(…) (Fragment), 217 OnCheckedChangeListener interface, 153 onClick(View) (onClickListener), 21 OnClickListener interface, 21 265 Index onContextItemSelected(…) (Fragment), 284, 286 onCreate(Bundle) (Activity), 16, 53-55 onCreate(…) (Fragment), 140 onCreate(…) (SQLiteOpenHelper), 543 onCreateActionMode(…) (ActionMode.Callback), 289 onCreateContextMenu(…) (Fragment), 284 onCreateDialog(…) (DialogFragment), 213 onCreateLoader(…) (LoaderCallbacks), 566 onCreateOptionsMenu(…) (Action), 257 onCreateOptionsMenu(…) (Fragment), 257, 258 onCreateView(…) (Fragment), 140 onDestroy() (Activity), 53 onDestroyActionMode(…) (ActionMode.Callback), 289 onDraw(…) (View), 522 onHandleIntent(…) (IntentService), 467 onItemCheckedStateChanged(…) (MultiChoiceModeListener), 289 onListItemClick(…) (ListFragment), 183 onLoaderReset(…) (LoaderCallbacks), 566 onLoadFinished(…) (LoaderCallbacks), 566 onLocationChanged(…) (LocationListener), 529 onOptionsItemSelected(…) (Fragment), 261 OnPageChangeListener interface (ViewPager), 207 onPageScrolled(…) (ViewPager), 208 onPageScrollStateChanged(…) (ViewPager), 208 onPageSelected(…) (ViewPager), 208 onPause() (Activity), 53 onPostExecute(…) (AsyncTask), 430 onPrepareActionMode(…) (ActionMode.Callback), 289 onPrepareOptionsMenu(…) (Fragment), 477 onProgressChanged(…) (WebChromeClient), 509 onProgressUpdate(…) (AsyncTask), 432 onReceivedTitle(…) (WebChromeClient), 509 onRestoreStateInstance(…) (View), 524 onResume() (Activity), 53, 197 onResume() (FragmentManager), 197 onSaveInstanceState(…), 241, 243 onSaveInstanceState(…) (Activity, 65-67, 69, 70 onSaveInstanceState(…) (Fragment), onSaveStateInstance() (View), 524 140, 237 onSearchRequested() (Activity), 453 onStart() (Activity), 53 onStop() (Activity), 53 onTextChanged(…) (TextWatcher), 141 onTouchEvent(…) (View), 518 OnTouchListener (View), 518 onUpgrade(…) (SQLiteOpenHelper), 543 open(…) (Camera), 304 openConnection() (URL), 418 openFileOutput(…), 279 options menus accessing on pre-Honeycomb, 259 android:showAsAction, 256 compatibility and, 283 creating, 257 defined, 253 determining selected item, 261 populating with items, 257 pre/post-Honeycomb behavior, 260 responding to selections, 260 updating dynamically, 477 updating dynamically (post Honeycomb), 477 organizing imports, 19, 24 OutputStream, 276 OutputStreamWriter, 276 overflow menu, 256 @Override, 56 P PackageManager class, 357 metadata and, 265 queryIntentActivities(…), 357 packages, naming, 3 padding, 156 Paint, 522 Parcelable, 524 parsing XML, 426 PendingIntent details, 474 getBroadcast(…), 530 getService(…), 473 location updates and, 529 persistent data with JSON, 273 using shared preferences, 461 perspectives 597 Index DDMS perspective, 75 Debug perspective, 80 defined, 75 Java perspective, 75 resetting, 75 switching between, 75 PointF, 519 post(…) (Handler), 448 PreferenceManager getDefaultSharedPreferences(…), 461 preferences (Eclipse), 33 preloading, 450 processes, 384, 385 progress indicator hiding, 509 spinner type, 320 updating from background thread, 432 ProgressBar, 320 projects adding resources, 46 configure, 3 creating, 3-6 gen directory, 16 layout, 6 library, 294 res/layout directory, 16 res/menu directory, 255 res/raw directory, 228 res/values directory, 16 setting package name, 3 setting project name, 3 src directory, 15 protection level values, 495 publishProgress(…) (AsyncTask), 432 putCharSequence(…); (Bundle), 195 putExtra(…) (Intent), 193 putInt(…); (Bundle), 195 putSerializable(…) (Bundle), 195 Q queryIntentActivities(…) (PackageManager), 357 R R, 16 randomUUID(), 133 read() (InputStream), 598 418 Recents button, 60 recycle() (Bitmap), 337 registerForContextMenu(…) (Fragment), RelativeLayout, 185, 186 285 release key, 26 release() (Camera), 304 release() (MediaPlayer), 234 remove(…) (FragmentTransaction), 208 request code (Activity), 104 requestLocationUpdates(…) (LocationManager), 530 res/layout directory, 16 res/menu directory, 255 res/raw directory, 228 res/values directory, 14, 16 ResolveInfo, 377, 378 resource IDs, 16-18 + prefix in, 18, 187 multiple layouts and, 165 syntax, 187 resources (see also configuration qualifiers, drawables, layouts, menus, strings) adding, 46 alias, 363-365 alternative, 247-250 default, 246 defined, 16 directory structure, 251 localizing, 245, 246 location of, 16 naming requirements, 251 referencing in XML, 47 string, 13, 14 styles, 392, 393 restartLoader(…) (LoaderManager), 562 result code (Activity), 105 retained fragments, 237-243 retainInstance property (Fragment), 237, 238 rotation activity lifecycle and, 61-65 before fragments, 243 onSaveInstanceState(…) and, 241, 243 preventing, 304 saving data across, 65-67 running on device, 44, 45 Index S prefix for variable names, 33 scale-independent pixel (see sp) screen orientation, 62 forcing, 304 screen pixel density, 46, 154, 246 screen size, determining, 373, 374 ScrollView, 347 SD card, 273 SDK versions (see also compatibility) build target, 116 codenames, 113 installing, xxi listed, 113 minimum required, 115 target, 115 updating, xxii search configuration, 456 configuring emulator for, 458 coordinating with SearchManager service, 456 hardware button, 453 how it works, 458 integrating into app, 451 intent filter, 457 metadata, 457 post Honeycomb, 463 program-initiated, 453 specifying details of, 465 searchable activity, 458 searchable.xml, 455 s SearchView bug, 465 class, 463 getSearchableInfo(…), 464 selector (XML element), 292 services adding to manifest, 468 bound, 481 lifecycle of, 480 locally bound, 482 non-sticky, 481 notifying user, 478 purpose of, 467 remotely bound, 483 sticky, 481 setArguments(…) (Fragment), 195 setChoiceMode(…) (ListView), 288 setClassName(…) (Intent), 380 setComponent(…) (Intent), 380 setContentView(…) (Activity), 16 setDisplayHomeAsUpEnabled(…) (ActionBar), 263 setEmptyView(…) (AdapterView), 270 setHasOptionsMenu(…) (Fragment), 258 setJavaScriptEnabled(…) (WebSettings), 507 setListAdapter(…) (ListFragment), 181 setOffscreenPageLimit(…) (FragmentStatePagerAdapter), 206 setOnClickListener(…), 21 setOnItemClickListener(…) (GridView), 503 setOnTouchListener(…) (View), 518 setPositiveButton(…) (AlertDialog.Builder), 213 setPreviewDisplay(…) (Camera), 308 setRepeating(…) (AlarmManager), 474 setResult(…) (Activity), 105, 106, 199 setRetainInstance(…) (Fragment), 237 setTargetFragment(…) (Fragment), 221 setter methods (see getter and setter methods) setText(…) (TextView), 104 setTitle(…) (AlertDialog.Builder), 213 setType(…) (SurfaceHolder), 306 setView(…) (AlertDialog.Builder), 215 shadow effect (adding), 404 shape drawables, 401 shared preferences, 461, 462 SharedPreferences, 461 SharedPreferences.Editor, 462 shouldOverrideUrlLoading(…) (WebViewClient), 507 show() (Toast), 23 show(…) (DialogFragment), 214 simulator (see emulator) SingleFragmentActivity, 172, 174-176, 361 singletons, 169 solutions file, 45 sp (scale-independent pixel), 155 SQLite, 541-560 SQLiteDatabase, 541, 556 SQLiteDatabase query(…) (SQLiteDatabase), 556 SQLiteOpenHelper, 541, 543 src directory, 15 599 Index T stack traces in LogCat, 76 logging of, 78 startActionMode(…) (Activity, 293 startActivity(…) (Activity), 99 startActivityForResult(…) (Activity), startActivityForResult(…) (Fragment), startPreview() (Camera), 308 startSearch(…) (Activity), 465 state list (drawable), 402-404 , 291, 402-404 status bar, hiding, 315 stop() (MediaPlayer), 234 stopPreview() (Camera), 308 stretchable (drawable), 406 string resources creating, 14 defined, 13 referencing, 48 strings file default, 14 defined, 13 strings.xml, 14 styles, 392, 393, 401, 404 defined, 153 inheritance, 398 themes and, 154 styles.xml, 392 support library, 131, 132 SupportMapFragment, 575 TableLayout, 230 TableRow, 230 104 199 @SuppressLint("HandlerLeak"), 444 @SuppressWarnings("deprecation"), 306 @SuppressWarnings("unchecked"), 445 Surface client, 306 defined, 306 lifecycle of, 306 SurfaceHolder interface setType(…), 306 using, 306-309 SurfaceHolder.Callback SurfaceView system icons, 256, 257 600 TextView example, 9 inheritance, 50 setText(…), 104 TextWatcher interface, 141 themes, 154, 231 threads background (see background threads) main, 421 message queue, 438 processes and, 384 as sequence of execution, 421 UI, 421 Toast, 23 toasts, 22-24 touch events handling, 518-521 intercepting, 321 U interface, 308 defined, 300 getHolder(), 306 SurfaceHolder and, -sw600dp suffix, 364 tablets creating virtual devices for, 359 user interfaces for, 359-369 TAG constant, 54 target fragment, 221 target SDK version, 115 @TargetApi annotation, 119 targetSdkVersion, 115, 116 task manager, 60, 380 tasks and Back button, 380 defined, 380 v. processes, 384, 385 temporal navigation, 262 306-309 UI fragments (see fragments) UI thread, 421 Up button, 262-264 Uri.Builder appendQueryParameter(…), 424 for escaping query strings, 424 URL for making URL from string, 418 openConnection(), 418 URLConnection, 418 Index user interfaces defined by layout, 2 for tablets, 359-369 laying out, 8-15 styles for, 392, 393 themes, 231 uses-feature, 304 uses-permission, 303 uses-sdk, 115 UUID.randomUUID(), 133 V variable names conventions for, 33 prefixes for, 33 Variables view, 81 versions (Android SDK) (see SDK versions) versions (firmware), 113 video playback, 227, 236 VideoView, 236 View (see also views, widgets) draw(), 522 invalidate(), 521 OnClickListener interface, 20 onDraw(…), 522 onRestoreStateInstance(…), 524 onSaveStateInstance(), 524 onTouchEvent(…), 518 setOnTouchListener(…), 518 subclasses, 8, 50 view layer, 35 view objects, 35 ViewGroup, 12, 63 ViewPager, 201-210 in support library, 204 internals of, 209 OnPageChangeListener, 207 onPageScrolled(…), 208 onPageScrollStateChanged(…), 208 onPageSelected(…), 208 views (see also widgets, layouts) creating, 516 custom, 516-518 persisting, 524 simple v. composite, 516 touch events and, 518-521 using fully-qualified name in layout, 517 virtual devices (see also emulator) creating, 24 for tablets, 359 testing low-memory handling, 69 W web content browsing via implicit intent, 503 displaying in browser, 501 displaying in WebView, 501 displaying within an activity, 503 enabling JavaScript, 507 web rendering events, responding to, 507 WebChromeClient for enhancing appearance of WebView, 508 interface, 508 onProgressChanged(…), 509 onReceivedTitle(…), 509 WebSettings, 507 WebView for presenting web content, 504 handling rotation, 509 WebViewClient, 507 widgets attributes of, 11, 156 Button, 9, 50 CheckBox, 151 DatePicker, 215 defined, 8 defining in XML, 11-13 EditText, 137 FrameLayout, 63 ImageButton, 50 LinearLayout, 9, 12 padding, 156 references, 20 ScrollView, 347 TableLayout, 230 TableRow, 230 TextView, 9 in view hierarchy, 12 as view layer, 35 wiring in fragments, 141 wiring up, 19 601 Index wrap_content, Writer, 276 X 13 suffix, 364 XML Android namespace, 12 referencing resources in, 47 XML drawables (see drawables) XML parsing, 426 XmlPullParser, 426 -xlarge 602
Source Exif Data:
File Type : PDF File Type Extension : pdf MIME Type : application/pdf Linearized : No Page Count : 625 Language : en Page Layout : SinglePage Page Mode : UseOutlines XMP Toolkit : 3-Heights(TM) XMP Library 4.4.43.3 (http://www.pdf-tools.com) About : uuid:3c472b8d-1109-474e-a4a1-ca6bc87c6f3b Keywords : PDF Version : 1.6 Producer : 3-Heights(TM) PDF Producer 4.4.43.3 (http://www.pdf-tools.com) Create Date : 2013:09:16 13:01:50-06:00 Creator Tool : DocBook XSL Stylesheets with Apache FOP Metadata Date : 2015:05:21 08:42:20Z Modify Date : 2015:05:21 08:42:20Z Document ID : uuid:14370528-fec4-4c95-a682-1cbc90ee559d Instance ID : uuid:07a63390-30e7-11e3-9994-109add4446cd Format : application/pdf Title : Android Programming - The Big Nerd Ranch Guide Creator : Bill Phillips, Brian Hardy Subject : Description : Code Mantra 002 C0020 LLC : http://www.codemantra.com Universal 0020 PDF : The process that creates this PDF constitutes a trade secret of codeMantra, LLC and is protected by the copyright laws of the United States Universal PDF : The process that creates this PDF constitutes a trade secret of codeMantra, LLC and is protected by the copyright laws of the United States Code Mantra LLC : http://www.codemantra.com Author : Bill PhillipsEXIF Metadata provided by EXIF.tools