Now that the most interesting parts of the UI are in place it’s time to show more interesting data. For now DbTradeAlert will download quotes on demand. At a later stage that will happen automatically every hour, too.

1. Add a Refresh Menu Item

Android Studio conveniently creates a menu for each app and therefore you just need an additional menu item to be able to communicate a demand for new quotes. But this item should be readily accessible unlike the Settings item that is hidden in the overflow menu – three vertical dots to the right of the app’s name.

1.1 Create the Icon

For life outside the overflow menu a menu item needs an icon. You can either provide your own, use one from the SDK’s icon set, or download one from Google’s Material icons. If you choose to use Android’s standard icons remember to copy them to your project as they change with Android versions.

As always I convinced Android Studio to do all the work for me:

In Android Studio’s Project view locate the “res/drawable” folder

In the folder’s context menu select New | Image Asset

In the Generate Icons window, Configure Image Asset step

Select “Action Bar and Tab Icons”

Enter “ic_action_refresh” as Name

Choose fitting Clipart

Select “HOLO_DARK” as Theme because the icon will have a blue backround

Click Next

In the Confirm Icon Path step click Finish

The Project view now will show a subfolder named “ic_action_refresh.png” with four files in the drawable folder. But if you go to the drawable folder on disk it’s empty. Instead there are subfolders like “drawable-hdpi” and “drawable-mdpi” that each contain a version of the icon for a different screen density. When the app runs it will automatically use the best fitting icon.

1.2 Create the Menu Item

Menus are defined by xml files in “…\app\src\main\res\menu” and the apps current menu is in menu_watchlist_list.xml. Add a menu item to refresh quotes:

Of course this menu item references the new icon. It also has a “showAsAction” attribute of “ifRoom” which means the icon will show up to the left of the overflow menu. Note that the attribute’s namespace is “app” while the other attributes are from the “android” namespace. As a nice addition Android Studio will show the icon’s preview on the left side of the editor.

A quick sanity check before moving on:

Test the app – the icon shows up to the left of the overflow menu; the Settings item is still in the overflow menu

Optional: commit the changes

The icon’s color is actually a light gray while the app’s title and the overflow menu’s dots are white. But that’s still better than anything my drawing skills would produce.

2. Ask for Permissions

Android apps traditionally needed to specify everything they want to use – like contacts, camera, or SD card – in their manifest so the user could make an informed decision whether to install the app.

Marshmallow changed that and apps now request a permission when they need it for the first time. This way the user gets an idea about why the app is requesting a permission. Of course the user can deny the permission or revoke it later and from Marshmallow on apps have to deal with that.

But DbTradeAlert gets lucky here: it only uses so-called normal permissions – like accessing the Internet – which are granted automatically. If your app uses so-called dangerous permissions – like access contacts or camera – make shure it can deal with denied and revoked permissions if it runs on Marshmallow and targets API levels starting with 23.

Google distinguishes between normal and dangerous permissions by how they affect the user’s privacy and the device’s operability. For example an app needs no permission to access its own directory on the SD card (unless running on API level 18 or lower) but accessing any directory outside that is considered dangerous.

DbTradeAlert needs to access the internet and check the network state beforehand. That leads to two permission requests in AndroidManifest.xml:

3. Download Quotes

To download quotes will take a few seconds even on a good connection. And waiting for the download to finish would make the app unresponsive which is an absolute no-no for any app. In fact Android will show the dreaded ANR (Application Not Responding) dialog if the app is unresponsive for 5 seconds. And that’s just asking to get uninstalled.

That’s why operations like downloads need to execute on a background thread and the usual solution is to derive a class from AsyncTask, do the work in doInBackground() and signal completion from onPostExecute(). Let’s start with getting the infrastructure ready and download the quotes. Storing them and updating the UI will stay on the TODO list.

The 3 provided generic types when extending AsyncTask determine the parameter types for the overridable handlers doInBackground(), onProgressUpdate(), and onPostExecute(). QuoteRefresherAsyncTask only needs a parameter for doInBackground() which is the Application context needed to create an instance of DbHelper. onProgressUpdate() isn’t needed as the data will arrive almost instantaneously and onPostExecute() doesn’t need a parameter as it won’t access doInBackground()’s result.

An additional Format parameter like “aa2bc4d1ghl1nopp2st1vx” controls what fields each quote should contain: “a” means Ask, “a2” means Average Daily Volume, and so on. The format parameter’s value is provided by DbHelper because it determines the order of columns which DbHelper has to parse afterwards.

The last parameter determines the symbols for which to get quotes and has a “+” between each symbol like “NESN.VX+NOVN.VX”. getSymbolParameterValue() creates that string and URL encodes it because index symbols start with a “^” which is verboten in URLs.

Next, doInBackground() needs to check if the app can access the Internet by calling isConnected(). As only a small amount of data will be downloaded – 488 bytes for the 4 sample stocks’ fields – restricting that only to Wi-Fi access isn’t necessary and currently the user has to explicitly request the download anyway. The NetworkInfo object could provide that information and a lot more like whether roaming is active.

When Internet is available downloadQuotes() creates a connection and if that is successful calls getStringFromStream() to read the downloaded data. The result passed back to doInBackground() looks like this:

This also shows how to get the Application context from inside an Activity – no need for an Application class. Note that onOptionsItemSelected() returns true when it handled the action and otherwise lets the super class deal with it.

That’s it for downloading quotes and the TODOs will be addressed in the next sections.

4. Parse and Store Quotes

Once the quotes have been downloaded DbHelper.updateOrCreateQuotes() needs to parse and store them:

The first step is to split the csv file into its individual rows and then to split each row into its individual values. All values except numbers are surrounded by double quotes which are stripped next.

Then each value in the current row is extracted according to the order defined by FormatParameter. Most values have to be converted to Float, Integer, or Date. getFloatFromString() and getIntegerFromString() simply call the type’s parse methods and return Float.NaN respectively null when that fails. getDataTimeStringFromStrings() basically does the same but for two values except that it needs to get around some quirks regarding “am” / “pm” indicators.

A quick note regarding exception logging: one should use Android’s logging infrastructure by calling Log.e() to get the most out of the logcat window’s features. Using e.PrintStackTrace() doesn’t help with that. On the other hand the Log.e() overload that takes a Throwable expects additional context info in its 2nd parameter. I haven’t found that useful as the log message already contains the stack trace and the value that threw up the parse method.

Now that the values have been extracted they are ready to go into the database – if the value of lastTrade could be determined. If not that’s usually indicative of an invalid symbol and the data is simply ignored. Otherwise after stuffing everything into a ContentValues object the code just tries an UPDATE as this will only fail for a newly added security. If the UPDATE fails a new record is INSERTed.

Note that while SQLite has an upsert statement (INSERT OR REPLACE) it doesn’t do an update but a delete followed by an insert! For autoincrementing primary keys that means the record gets a new primary key which obviously wrecks the database.

The final step is to call updateOrCreateQuotes() from QuoteRefresherAsyncTask.doInBackground():

Test the app by tapping the Refresh icon – as DbTradeAlert still lacks the means to update its UI you need to restart the app to see the updated quotes

Optional: commit the changes

6. Update the UI

Updating the UI after the new quotes are stored will require two additions to DbTradeAlert:

From QuoteRefresherAsyncTask inform WatchlistListActivity that it should update its UI

Extend WatchlistListActivity so that it can update its UI

Let’s tackle them one at a time.

6.1 Communicate from QuoteRefresherAsyncTask to WatchlistListActivity

Communication from an AsyncTask to an activity has a problem: Android can destroy any activity on a whim. For example the current activity will be destroyed and recreated when the user changes the device’s orientation so the activity can use a different layout. Passing that activity to QuoteRefresherAsyncTask would create a memory leak because the additional reference prevents the activity from being garbage collected. The AsyncTask would also communicate with a defunct activity. And using a WeakReference would only solve the first part of the problem.

Broadcasts to the rescue! QuoteRefresherAsyncTask declares what intent – that is an object describing a desired action – it will broadcast and uses LocalBroadcastManager to send the intent. While it’s alive WatchlistListActivity will dynamically register itself as a recipient for this kind of intent and use a BroadcastReceiver to pick them up:

DbTradeAlert uses a local broadcast because it’s more secure and more efficient than broadcasts that can reach other apps.

Some additional info about this usage of broadcasts and intents because the next post will build on it: intents that use a string to define their action are called implicit intents because they specify their recipients only implicitly – Android uses intent filters to find out which classes will receive the intents. Explicit intents specify their recipients explicitly by specifying the recipient’s class. For performance and security reasons you would prefer explicit intents. But in this case a LocalBroadcastManager is used which already provides the performance and security benefits. Another reason to use an implicit intent is that dynamically registered BroadcastReceivers cannot receive explicit intents.

To try the new messaging infrastructure first set a filter of “quoteRefresherMessageReceiver” in Android Studio’s logcat window. Then

To clear logcat when running this repeatedly make shure to use the Clear button to the left of the logcat window. This will clear the device’s logcat, too. The context menu’s Clear item won’t do that.

What could still happen is that the activity get’s destroyed after starting a download and that finishes before the new activity has registered itself as a receiver. I can live with that and just tap Refresh again.

When you just need to update a View a possible solution is to implement the AsyncTask as an inner class of the activity. You can then use findViewById() in onPostExecute() to get hold of the View because onPostExecute() runs on the UI thread.

For each watchlist in the database the local ViewPager instance is asked to find a corresponding RecyclerView instance – the required tag will be added in the coming steps. When that RecyclerView is found its WatchlistRecyclerViewAdapter.changeCursor() method – not added yet either – is called with the new quotes.

DbHelper.closeCursor() checks if the cursor isn’t null and then calls its close() method.

With the sample data all RecyclerViews will be found. If you create many watchlists those that you haven’t looked at yet will not exist yet and findViewWithTag() will return null. That’s OK because they will show current data when they get created later anyway.

The next step is to call refreshAllWatchLists() from BroadcastReceiver.onReceive():

So refreshAllWatchLists() is called when QuoteRefresherAsyncTask has already stored the new quotes. After updating the UI DbTradeAlert adds a timestamp to its title – determined from the app’s resources – so the user can see at a glance when the quotes were last updated.

Remove the timestamp in WatchlistListActivity.onOptionsItemSelected() so the user has an easy way to see whether quotes are still updating:

This code also shows the effort needed to keep the timestamp once it has been added:

If Android thinks the activity is about to be recreated it calls onSaveInstanceState() so activities can save what’s needed. That instance state is then provided in onCreate() and in onRestoreInstanceState(). A user rotating the device will create this scenario.

This will not happen if the user switches the screen off or uses the back button. In this case only onPause() and onResume() will be called and savedInstanceState will be null in onCreate(). DbTradeAlert simply uses a static field to keep the title’s value in this scenario.

The third scenario is for example when a user swipes the app away from the recent apps list – in this case even onPause() isn’t called.

Now let’s add the missing pieces. First create a changeCursor() method in WatchlistRecyclerViewAdapter:

After changing the cursor a call to the super class’ notifyDataSetChanged() is all that’s needed to have the UI update itself. Some things to note:

The cursor field isn’t final anymore

notifyDataSetChanged() has specialized overloads available in case you know which items have changed. They avoid unnecessary redraws and also provide nice and consistent animations if for example an item was removed

CursorAdapter.changeCursor() and CursorAdapter.swapCursor() sound similar but only CursorAdapter.changeCursor() closes the old cursor

The only missing piece now is the RecyclerView’s tag. That’s a single line in WatchlistFragment.onCreateView():