Intro
For this lab you will be developing a new Application named AppRater that suggests other Applications for users to download and try. The purpose of the application is to share fun and interesting applications with other users. The users can then rate the applications.
Objectives
At the end of this lab you will be expected to know:
Activities
For this lab you will be working with a brand new application, completely independent of the previous labs. Once you have a general understanding of the components of the application you will begin implementing it. You will be given more general instructions in some sections. You should be comfortable enough with Android by now that you can figure more out on your own.
IMPORTANT:
You will be given a Skeleton Project to work with. This project contains all of the java and resource files you will need to complete the lab. Some method stubs, member variables, and resource values and ids have been added as well.
1. Setting Up
1.1 Creating the Project
To begin, you will need to download and extract the skeleton project for the AppRater application.
Extract the project, making sure to preserve the directory structure.
Take note of the path to the root folder of the skeleton project.
Next you will need to setup an Android project for this app. Since the skeleton project was created in Eclipse, the easiest thing is to import this project into Eclipse.
Make sure to set up your project for using ActionBarSherlock. See Labs 3, 4 and 6 for examples of this.
Make sure the Manifest SDK targets the latest version of Android (17 at the time of updating this lab) and that the minimum API target is set to 10 (Gingerbread).
1.2 Familiarize Yourself with the Project
The project contains a sizable number of Java files and several XML files. This app has no contextual action menu, since everything is handled from within the application views or various Intents.
AppRater.java will contain the definition for the main AppRater Activity class. This is the class that will display the list of applications the user is supposed to test and rate. The AppRater class makes use of a very simple XML layout file called app_list.xml which has been completed for you. It is composed of a ListView that displays a list of applications that a user is supposed to try out.
Applications that users are supposed to try out and rate are represented by the App model class, defined in App.java. This is a simple class that encapsulates the name of the application, a rating for it, the Market-URI from which the app can be downloaded (we'll discuss this later, but think of it as a link to installing it from the Market application for now), a boolean used to flag the application as installed, and a unique float ID.
The AppView class is a custom class that is used for visualizing the state of an App object. Similar to how JokeView and Joke worked in Lab 4, the AppView class has an App member variable on which it bases the state of its display. Its layout is defined in the app_view.xml XML layout file, which you will will out shortly.
An AppView has three different "states":
The AppRater Activity gets the Apps it will display from the AppContentProvider class as a Cursor of Apps. This is quite similar to how the JokeList Activity in Lab 4 got a Cursor of Jokes from the JokeContentProvider. The AppRater Activity's ListView then uses the AppCursorAdapter class to bind AppViews to the cursor of Apps. If you are not familiar with Content Providers, it is strongly recommended that you complete Lab 4 before continuing this lab.
The AppRater Activity starts the AppDownloadService in order to add new Apps to the underlying database. The new Apps are retrieved from a website through this Service. Using a Service is like launching a new Activity except that there is no User Interface for the Service. The AppDownloadService then downloads and inserts new Apps into the database if they do not already exist, and occasionally checks for new Apps to add as long as it is running. In order to insert new Apps into the database the AppDownloadService must use the AppContentProvider's insert(...) method via accessing a Content Resolver. This is similar to the download functionality covered in Lab 5.
Once the AppDownloadService has successfully added a new App, it broadcasts a special Intent with a certain action associated with it. The AppRater Activity will use its internal DownloadCompleteReceiver class to listen for and match that special Intent. When it sees the Intent it knows that it has to update the list of apps in AppRater with a new Cursor.
Once an App is downloaded into the list, it may then be installed by clicking on it to launch the Google Market/Play Store that shows the information about the App as it appears in the store. The user can then test the app freely outside of the AppRater application. The Apps that are installed are marked as such in the ListView via a non-interactive CheckBox as well as the background color mentioned above.
Lastly, a user can give each App a rating. The user can apply a rating to an App via a RatingBar. The AppRater Activity then saves the changes to the App through the AppContentProvider using a custom OnAppChangeListener (similar to the OnJokeChangeListener from Lab 4).
The following classes/files have been fully filled in for you:
You will fill in the remainder of these classes/files:
Study the classes and files that have been fully filled in for you and make sure you understand what's going on inside them before moving on. In particular, the Content Provider classes should be familiar from Lab 4: AppRater will store all App data inside of a database wrapped by a content provider, which provides an interface to the main application for interacting with the database in the form of a table.
2. AppView
Similar to how Lab 4 had JokeView, AppRater has an AppView class to implement. This is the View representation of an App in the list of Apps, and contains data from an internally stored App object.
In this section you will implement the XML layout for the AppView class, and then fill out the AppView class itself. There will be some minor modifications to make to other classes along the way as well.
2.1 AppView XML Layout
Each AppView will have three main components: A TextView containing the name of the app, a non-interactive CheckBox (meaning the user cannot change its checked state) and a RatingBar that lets the user dynamically change their rating of the app.
When you have finished implementing the XML layout file, it will look something like this (click image for full view):
Remember that this is just one instance of the AppView. It will scrunch to fit minimally inside the ListView.
Before you begin, let's talk RatingBars and why it gets its own row in the above image.
2.1.1 About Android's RatingBar
A RatingBar is something you've seen in Android before. When you install an application from the Google Play Store, you see a "Rate and Review" area on an app's Play Store informational/install page (if you need a refresher, there is an image in section 3.1.2 below that demonstrates this) and it's got a RatingBar for rating the application.
RatingBar is a great app component in theory--it acts as a subclass of a progress bar, so the user can touch a star to give the RatingBar a rating or slide their finger along the RatingBar until they release it, and then the rating is set. You can change the size of the default RatingBar in two ways, modify the step size (e.g. to enable half-star ratings such as 3.5 or 4.5 out of 5 stars, set the step size as 0.5), the number of stars and set the current rating at any time. Furthermore, you have the option of styling the RatingBar to add custom rating "star" images, sizes and progressive drawing (meaning, Android has a built-in mechanism to automatically fill in an image in the bar based on step size, so you only need to provide a full and empty "star" image!). Finally, it has its own Listener for detecting a rating change.
Unfortunately, there are just as many problems with this component as there are perks. For starters, read the Class Overview section in the RatingBar documentation. You'll notice some immediate restrictions:
You can choose to style your own RatingBar to change its size and other properties while still preserving interactivity, but then you must worry about supporting multiple screen densities and sizes, which means you are stuck having to create a series of icons for each density or be forced to limit the size of your stylized RatingBar to the size of the custom icons. Even then, you may see a stretched 'bleed' effect on custom rating bar styles.
Feel free to experiment with stylizing the RatingBar in your application, but don't make it your life's mission. RatingBar is very restrictive and unpredictable.
2.1.2 app_view.xml
Now that you know how unruly the default RatingBar component can be (and that you're stuck with it unless you want to take a crack at stylizing it) you can plan your implementation of the XML file around this outstanding issue.
Open up app_view.xml. If you preview it as-is in the Graphical Layout tab, everything is in disarray because the TextView text is not set and the default ViewGroup is a RelativeLayout without any organizational properties set.
You are tasked with filling in the remaining component attributes to make the view look like the above image. Hints follow:
This might help you catch problems that happen on some devices (but not all) more easily.
2.2 AppView.java
Now that we have our static layout, we can implement the Java class. Open AppView.java and examine the class again before continuing on to do the implementation.
2.2.1 Layout and App Initialization
Start by filling in the constructor for AppView.
Fill in the setApp(...) method.
2.2.2 OnRatingBarChangeListener
Similar to how the RadioButtons in Lab 4 had an OnCheckedChangeListener associated with them, the RatingBar class has its own listener: OnRatingBarChangeListener. You will now implement this in AppView.
Make AppView implement OnRatingBarChangeListener.
Now AppView is ready for action. Unfortunately, the rest of the pieces aren't quite ready yet, so you cannot test the application right now. After the next section that will change.
3. Services
In this section you will implement a Service called AppDownloadService that downloads a list of applications from a web-hosted file and populates AppRater's ListView with AppViews. This of course means that you will need to implement AppView, but we will first focus on the class where Apps are conceived in the application scope.
The flow of data in the application is similar to that of Lab 4, in particular the content provider data cycle, but contains the following extra behaviorisms:
-Download list of app data to populate ListView with AppViews
-Broadcast an intent when a new app is added
-Receive broadcast and launch Notification in Notification Bar that new app is ready
-Launch Intent to install app on user's device when an app in the list is touched
What are Services?
Simply put, a Service is like an Activity without a user interface that runs in the background. An application can start a Service, let it do its thing in the background, and still provide app functionality without any disruption to its user. Any time you need to perform long and/or routine tasks in your code, consider enlisting Services to help accomplish this. However, there are some key points to Services that must be understood before using them:
1. Services are not separate processes. When launched, they run in the same process as the application they were started in. This means you should not use them for work-heavy processes like playing MP3s. See the documentation on Processes and Threads for more information on how to run CPU-intensive asynchronous tasks.
2. Services are not threads. They are not a method for working off of the main running thread. However, IntentServices (which will be discussed shortly) do have their own thread.
3. Services can be accessed by other applications than the one that started them. This creates a sort of client-server interface, but requires much more code setup than running a service local to just one application. We will not be using this functionality, but for more information on this read the Bound Services documentation.
4. Services are not AsyncTasks, as similar as they may be. The latter is designed to run very short operations that do one thing and quickly exit. The former is designed to run for longer events and even when your application Activity is not open. Sometimes it's better to use one over the other. For our lab we want to run recurring code on a timer over more than just a few seconds, so we will not be using AsyncTask. For more information on AsyncTask read its documentation.
For more information and a guide on Services, look here.
3.1 AppDownloadService
The first step in this lab is to implement an IntentService called AppDownloadService. An IntentService is a Service started using an Intent. You already have experience starting something with an intent: Launching a device's Camera using an Intent (Lab 6). An IntentService is launched exactly the same way, but this time we are going to implement our own Intent instead of rely on Android's built-in one.
For more information and an example of an IntentService, look here.
3.1.1 Extend IntentService
Open up AppDownloadService.java and read the documentation for the methods you will implement. However, before you implement them you will first make AppDownloadService extend IntentService:
3.1.2 Get Apps From Server
As you can probably guess from the above subsection title, you will now fill in getAppsFromServer(). This method will fetch app data from a file hosted on a server and then translate that into a new App object that will be added to the ListView in the next subsection.
All app data will be on a single line. All data for an app is comma-delimited, with apps separated by a semicolon. The format of a single app's data you will be reading in is as follows:
AppName,market://details?id=app.package.name;
A formatted example of data in this file for three apps is given below:
Papyrus,market://details?id=com.steadfastinnovation.android.projectpapyrus;Flow Live Wallpaper,market://details?id=pong.flow.lite;Go Make It Rain,market://details?id=com.fireswing.makeitrain;
This corresponds to the following information about the apps:
The App Market Information URI is the URI corresponding to the app's information/installation page if you search for and touch the app in the Google Play Store. For example, if you went to the Google Play Store, searched for the Papyrus app and then clicked on it in the search results for more information, it would take you to the page that corresponds to its App Market Information URI. See below image for clarification (click image for full view):
This page in the Play Store corresponds to the URI market://details?id=com.steadfastinnovation.android.projectpapyrus. URIs are everywhere!
Anyway, let's get to downloading!
Fill in the getAppsFromServer() method.
3.1.3 Add New App
To no one's surprise, this subsection has you filling in the addNewApp(...) method. This is where the content provider app insertion occurs.
Fill in the addNewApp(...) method.
3.1.4 Announce New App
Finally, you will fill in the announceNewApp() method. This is where Intent broadcasting happens.
Fill in the announceNewApp() method.
That does it for implementing the IntentService. Now we have to actually include it in our application.
<service android:enabled="true" android:name="edu.calpoly.android.apprater.AppDownloadService"/>
<uses-permission android:name="android.permission.INTERNET"/>
3.1.5 Starting and Stopping Services
Now that we have our IntentService, we have to incorporate it into AppRater. To use a Service, you start it using startService(...). To stop a Service, you stop it using stopService(...). Simple enough, right? You will implement this start/stop functionality in AppRater's menu items.
Open up AppRater.java and look at the overridden onCreateOptionsMenu(...) method. Based on the inflation going on, there has already been a menu layout file created for you, mainmenu.xml. Take a look at that file and see what the menu will contain: three menu options. These options will allow users of the application to start or stop the service that downloads the apps from the server to the list and rechecks the server periodically for more, or remove all apps from the list.
In AppRater.onOptionsItemSelected(...):
In AppRater.fillData():
Hooray, now we can finally run the application! If you run it you should see something similar to the following (click image for full size):
You can assign ratings to apps that aren't installed by default. You can change this behavior later on if you'd like.
The above image contains screenshots from a continuous run of the application up until this step. Each screenshot shows what happens when the following actions are taken:
1: Starting the app up fresh
2: Pressing the "Get Apps" button
3: Rating several apps
4: Pressing the "Remove All Apps" button
The application should also persist ratings when you move away from the application by pressing Back or Home, and also when orientation changes. Just one of the many benefits of using a database for persistence!
Note that you can set an application's rating back to 0/5 stars. This is perfectly acceptable.
Now we have covered the following data flow segments:
-Download list of app data to populate ListView with AppViews
-Broadcast an intent when a new app is added
Now we need to actually catch the broadcast we sent. We'll do this and more in the next section.
4. The Broadcast Receiver
Great, so we've added apps to the database and told our application that there's a new app to review. Our application needs to catch wind of that broadcast somehow, and it will be done using a Broadcast Receiver. In this section, you will implement a custom BroadcastReceiver for catching Intents broadcast from AppDownloadService. You will then have this BroadcastReceiver (DownloadCompleteReceiver) show a Notification in the Notification Bar on your device every time there is a new app added to the list.
4.1 DownloadCompleteReceiver
A BroadcastReceiver is essentially a type of listener that watches for specific Intents launched via sendBroadcast(...). Our AppDownloadService already invokes this call, but so far we have not acted to catch it. By implementing a BroadcastReceiver, we not only gain complete control over what to do when a specific broadcast is received, but we can handle these Intents from potentially any application as long as we register the BroadcastReceiver properly! (We won't be doing this, but it's still nice.)
In this subsection, you will implement the DownloadCompleteReceiver class which is nested inside of the AppRater class. In DownloadCompleteReceiver, if a broadcast Intent with the action ACTION_NEW_APP_TO_REVIEW is received, we want to update the list of apps and post a Notification to the Notification Bar. You will implement the former and set up the latter in this subsection.
Fill in the onReceive(...) method in DownloadCompleteReceiver.
4.1.1 Registering and IntentFilters
You have to register a BroadcastReceiver with the application in order to properly listen for Intents. You will do this by calling registerReceiver(...) and unregisterReceiver(...), respectively.
Remember IntentFilters? You might not, considering they were pretty out of your way for the majority of these labs, but they've been right under your nose this whole time. You used them in every lab so far to indicate which Activity was the main Activity in your Manifest file, and now they're back for another round. When you register a BroadcastReceiver, you must pass it an IntentFilter so that it knows to search for Intents with appropriate categories. Earlier, you set the broadcast Intent's category to CATEGORY_DEFAULT, so you will add this as a category to an Intent Filter that gets paired up with your DownloadCompleteReceiver.
For more information about Intents and Intent Filters, see the guide here.
Properly register and unregister your DownloadCompleteReceiver.
4.2 Notifications
"You've got a new App to review!"
Probably the coolest feature introduced in this lab, Notifications are messages that show up in an Android device's notification area (sometimes referred to as the Notification Bar or Notification Drawer). They have two display types: Normal and Big. We will be creating Normal view Notifications in this step.
If you use an Android device on a regular basis, you are likely more than familiar with Notifications: They're those small notices you get from various apps and services. If you get an email in a Gmail account that's hooked up to your Android device, you get a notification. On newer devices, if you take a screenshot then you'll get a notification in the area too. These are just a few examples of many.
Below is a screenshot of the Notification Drawer in Android 4.2 (Nexus 7) with some sample Notifications (click image for full size):
You will obviously be implementing one of these.
Something you may not be familiar with is that Notifications have a very large pool of options on the programming side. Because of this, Google has added a Notification.Builder class in API 11 (Android 3.0, Honeycomb) to allow for easy Notification customization and creation. However, since we want our backwards-compatibility (and Google does too) we will use the NotificationCompat.Builder class that comes with the v4 Support Library. Take a look at that Builder class and see just how much we can do with Notifications!
Before you go completely nuts with Notifications though, be warned that Notification backwards-compatibility is not 100% functional across different versions of Android. For Notification guidelines, visit the Notifications Patterns page.
Once built, Notifications cannot post themselves, the NotificationManager has to be made aware of them and post them itself. To do this, Notifications use PendingIntents. A PendingIntent is basically a regular Intent--it contains some action to perform. But unlike regular Intents, PendingIntents grant the right to perform the action or operation in them to whatever they are given to to be executed. In this case, a PendingIntent allows the NotificationManager to post a Notification to the Notification area. The NotificationManager can be considered a 'foreign' application, and it will use your application's permissions instead of its own.
In DownloadCompleteReceiver, fill in the showNotification(...) method.
Run your application and make sure that the Notification ticker text pops up in the minimized notification area, then that the Notification is in the maximized Notification area (click image for full size):
Screen capture Notification included as a bonus.
The above image contains screenshots from a continuous run of the application up until this step. Each screenshot shows what happens when the following actions are taken:
1: Starting the app up fresh and pressing the "Get Apps" button
2: Observing Ticker text and Icon appear in minimized Notification area (Icon is not properly sized, this is expected if you choose too large of an icon unless you want to add your own that is the proper size)
3: Expanding Notification area to show App Rater Notification with properly-sized Icon
4: Touching the App Rater Notification minimizes Notification area and removes App Rater Notification
We have now finished implementing the third piece of new data flow in this application:
-Receive broadcast and launch Notification in Notification Bar that new app is ready
Now there's only one more thing to do!
5. App Installation & Play Store Intent
Your final task is to implement the final piece of new data flow in this application:
-Launch Intent to install app on user's device when an app in the list is touched
Right now, the application is a sea of red (or orange) because we have not been able to install any of these apps yet. That is about to change! You may have noticed that touching or pressing one of the AppViews has shown some level of interaction: certain components appear to be selected while holding down the touch or press on the AppView until you let go.
As you have probably anticipated from the beginning, you are left to implement this final task yourself. However, hints follow:
Run your application and make sure that you can now launch Play Store/Market Intents that show the application and allow you to install the application, and that an app changes appropriately when you install, rate or uninstall an application. A sample storyboard is presented below (click image for full size; warning, it's large!):
Certain screens may vary depending on device. Above is taken on a Nexus 7 running Android 4.2.
The above image contains screenshots from a continuous run of the application up until this step. Each screenshot shows what happens when the following actions are taken:
1: Fresh start of application after clearing Notification
2: Clicking S Pen Launcher app (can't install on Nexus 7, sad day)
3: Going back and clicking Papyrus app
4: Going through motions of installing Papyrus app
5: Installation of Papyrus app in progress
6: Installation of Papyrus app complete
7: Press back; Papyrus now yellow
8: Papyrus rated 5 stars; now green and ordered accordingly
9: Papyrus rated 0 stars; now yellow
10: Studious app rated 4 stars pre-installation
11: Going through motions of installing Studious
12: Fast-forward to where Studious is now installed; Studious now immediately green
13: App flipped to landscape orientation
14: About to uninstall Studious app (many ways to do this depending on device)
15: Uninstall Studious
16: Uninstall of Studious complete
17: Go back to App Rater
18: Studious now uninstalled; Studious still rated 4 stars and now red
In conclusion...
This lab was a brief introduction to Services, BroadcastReceivers and Notifications, with some other components thrown in for good measure. We hope you learned how to implement some really cool features, and that you can use this lab as a springboard for further Android development!
6. Deliverables
To complete this lab you will be required to:
Primary Authors: James Reed and Kennedy Owen
Adviser: Dr. David Janzen