Sat, 19 Aug 2017
Developing an Android app - Wallpapers app - part 5
This is my 5th post in a series about the Wallpapers Android app I developed that is on Google Play. The first blog post describes how I started developing it, this is about the last few versions I released.
In early 2017, Google favored a certain type of Model-View-Presenter architecture for Android apps. Google promoted this as the way Android apps should be written.
As my last blog post notes, on April 23rd, 2017, I began refactoring the entire app to fit more into this architecture that Google was promoting. In the three weeks after April 23rd, I did a large amount of work rewriting the app in this manner.
Then on May 17th, Google I/O happened, and they announced a whole new way of architecting apps that sort of junked my last three weeks of heavy work somewhat. C'est la vie! Welcome to Android development. Also, as typical for Google, it was announced as a beta, so the production readiness of it was questionable. After talking to people and reading thoughts from Android experts, I decided to press on with refactoring to this now deprecated architecture, with thoughts of perhaps refactoring it again to the new architecture model at some point in the future.
So my previous blog post focuses on the release of this majorly refactored code on June 13, 2017. This blog post focuses on the post-release of that. First, fixing errors I saw pop up on the release of that code. Also, other improvements I have made since that release.
June 15, 2017
I update the Google services JSON for Firebase (and Admob), and upgrade Firebase to v.11.0.1. On the Admob backend, ads for the main page are distinguished from ads on the category pages, so I make the distinction explicit in the ads on the app as well. I display a Toast when a download completes successfully. I also deal with when an object comes in as null in places where I am not 100% sure why the object would ever come in as null, I have to check into that more.
June 16, 2017
Release app, release 2.7.3. I do partial releases to 1000 users at a time of the new code.
June 19, 2017
When the Android client connects to the JSON API, each client sends a unique InstanceID. The main purpose of doing this is to track down errors, if people are having problems with the app, we want to have as much information as possible in order to try to fix the problem. However, I am seeing ANR (Application Not Responding) errors, as some Android devices are freezing up while calling the Google Play Services code to get an InstanceID. So I put the (not very essential) call in an AsyncTask so that that freeze-up does not happen.
There was also a problem of network requests going out, the view/presenter being reset and sending out a duplicate network request, then the old request coming back, and then the new duplicate one. The simplest thing for me to do is to discard the old ACK, so that is what I do. I do a release and start rolling out this new version.
June 21, 2017
More nullness to deal with. Retrofit Response bodies are coming back null. Have not been able to reproduce this in QA yet. I rewrite the code to display the "network failed" dialog when this happens, and have to do more QA to see how to reproduce this problem which is happening in the field.
It was not scrolling all the way to the end of the wallpaper grid in some cases, I modified the code so that would.
In the previous blog post, I mention one thing I punted on with the big June 13th release was ranged notifies. When a JSON would return new wallpapers, I notified and refreshed the entire adapter, which made the images reload (which made the screen blink) every time a new JSON came back with new wallpapers. As June 13th approached I was getting antsy with how long the refactor and QA had taken and decided this annoying blinking was something I could live with and deal with later. As the release went out, I take a look at it now. I see that it is not that difficult to send a notifyItemRangeChanged to the adapter, so I do that. The reloading and blinking is now gone. Yay.
I also make sure some assertions are true before loading more JSONs for the recent and popular wallpaper grids.
June 22-24, 2017
I am running into one of those hairy Android problems. There is an older and newer method of sending images off in an Intent to be set as wallpapers. The problem is it is not exactly clear when the old method should be used, and when the new method should be used. For the app being sent to, which method to use can depend on not only the app version, but the Android version, and other factors. Also, this new method has problems to be dealt with as well - the old version handles things like JPG's which have filenames which end with a capitalized JPG, but the new method does not (without some rearranging any how). I don't really fix anything, but Google+ is now excluded from setting wallpapers as it only works with the new method, which I have yet to implement (outside of test functions).
June 26, 2017
Some of my competitors have a nice feature graphic for their Google Play store listing. Mine is not so great. So I put together a nice 1024x500 feature graphic. What I do is find 14 nice wallpapers which go together nicely. Then I make 146x250 thumbnails of them, which are a ratio close to that of a typical Android phone. With the exception of the 4 wallpapers on the left and right edges, which are all 147x250 size.
I never did a store listing experiment before so I do one for the new graphic. I start by doing a global experiment, but the global experiment is constricted. So I do it by language - both English and French. I run the experiment for 11 days. There are a few hundred downloads but no big statistical difference is seen. So I end the experiments and serve everyone the new graphic - it doesn't seem to have harmed anything anyway. Subsequent looks at that statistics yield very little as well, it had no major affect on download conversion in either direction.
June 30, 2017
Deal with Retrofit Response being null for detail responses, just as I had for Retrofit Response being null for grid responses on June 21st. As a preventative measure, I have the category page deal with null Retrofit responses as well, although I have not yet seen them in the wild.
July 7, 2017
Even though the images on the category page have been shrunk to 200px, they still cause OutofMemory errors on some devices. So I push handling of the image loading to Glide.
July 8, 2017
Upgrade Firebase etc. to 11.0.2
July 9, 2017
People in the wild are crashing on a null view object in DetailFragment. I put in a kludge to deal with this, but the real problem is that object should not even exist in the first place, and DetailFragment has become too spaghetti code like as it has continually accreted code to try to deal with all the various tasks it has to do (permissions, load two thumbnails, load JSON and description, download and set wallpapers). A few weeks later I will rewrite this class and make it cleaner.
July 10, 2017
Production release of new code.
July 13, 2017
Usually I am testing this code on wifi. When I test it on a cell connection, my connection is usually good. So I don't have a lot of QA from less robust areas.
In New York City there is a local Android developer meetup. I go to it and show someone my app. The cell connection is not robust though, and embarrassingly, my app has problems as I show the app to someone. The problems go away when I go home to my wifi and good coverage area.
I go to a $150-a-month co-working space I have access to, where the cell coverage is not always robust. I begin to see the problem again. When the phone is on wifi, when in the fragment's onResume I ask the connectivity manager if the network is connected, it immediately says yes. However, when the question is asked while the phone is on a spotty cell connection, the answer within the first 20 milli-seconds to whether the network is conncted is "no". Usually about <20 milliseconds in, a system broadcast comes in that the network is connected.
So now in onResume, I do a network test, wait 100 millseconds (I tack on 80 milliseconds), then do a second network test. I only listen to the results of the second test. This seems to solve the problem, I get much less false "network disconnected" messages. Another Android programmer told me I must be imagining all of this, but this is what happened for me. Perhaps his phone never has this problem.
July 16, 2017
When I show the network is disconnected dialog, I have been assuming people were always clicking the OK button. I put in code to deal with every manner in which they might dismiss this dialog.
July 18, 2017
I add the timed network connection test to the category grid.
July 19-22, 2017
I write some JUnit and Espresso tests for the app. From Android development on Eclipse to now there have been many changes, but I see that it is very easy to write tests now. It just takes a few minutes to add a JUnit test and an Espresso test and then run both.
July 25, 2017
I QA the app on an ICS (v. 4.0) tablet. Oops, the permissions for downloading are not correct. Manifest.permission.READ_EXTERNAL_STORAGE was not introduced until API 16. I redo permissions so that the small amount of devices that still come in that are v4.0 API 14 and API 15 work.
Also, a few people here and there are having IllegalStateException errors when doing a DownloadManager.Request on the setDestinationInExternalPublicDir method. There are three possible IllegalStateException's they may be having, and I don't know which one they are generally having. So I set up a method to test for this and upload data to my bug reporting server if the problem is seen. I will be looking into this more as reports come in (although so far, people have been having two of the three possible errors, pointing to different causes).
People are using new licenses on Wikimedia Commons so I add blurbs about those new licenses to the app. There are enough wallpapers in the Sky category (60) to put it into the app, so I do so and put a relevant drawable in for it as well.
Also, in unexpected behavior news, some people click the download button 10 times in a row and download the wallpaper 10 times. So now I have it download on the first click and ignore subsequent clicks.
July 26-30, 2017
More JUnit and Espresso tests.
August 1-12, 2017
Dealing with that problem with people clicking download 10 times in a row on the Detail page, I want to add more state to the Detail Fragment, but I take a look at it and see how much spaghetti code it has. Dealing with loading the existing small thumbnail, and then a larger thumbnail, dealing with grabbing and displaying meta information, dealing with permissions, and downloading and setting - the code has accreted and is now fairly convoluted. I do a JavaDoc generation of the project and look at the detail code in the JavaDoc and it is not pretty. I also manually put together a Graphviz of the DetailFragment method call graph and it is convoluted and confusing.
Instead of accreting even more functionality to an already convoluted class with a lot of spaghetti code, I decide to refactor the class. I start from scratch and cut and paste the old code as needed.
One of the first things - as I mentioned on July 9th, I was keeping the view object around in the Fragment, which was not a good idea. So I dumb that and now just getView() when I need the Fragment's View.
I also have a variety of Strings and such scattered about with information on the wallpaper images and the wallpaper metadata. I consolidate that into two classes - Wallpaper and WallpaperMetadata.
The code had just accreted and had kludges and was calling things unnecessarily. I streamline to a sensible directed graph. When the fragment resumes, I load the small thumbnail, and have the larger thumbnail laod after that. I also have another directed graph where a JSON of metadata is pulled and then displayed on the page. The third directed graph is based on the download and set buttons. If pressed, I check for the proper permissions, and based on that, download, and if requested, set the wallpaper.
This is better than the previous code, which had unneeded dependencies in the image load and the metadata load, and other unneeded dependencies. Everything is now off in its own self-contained silo of functionality.
The network failed dialog is still popping up when it should not sometimes (after onInstanceState being called for instance), so we deal with that as well.
August 13, 2017
Somone with a small, low density phone gived the app a 3 rating. I make an emulator for a phone of this type and test it out. I see the word categories on the tab appears in a font which is too large, so I decrease the font size on small, low density devices.
August 15-17, 2017
More Detail fragment refactoring. Rewrite JUnit tests for the Detail presenter, as I modified the Detail presenter as well.
On the server side, since I'm a full stack programmer [at least according to the definition I read someone give online somewhere of what a full-stack programmer was :) ], my Python script which determines which wallpapers are popular was running slow because it was taking too long to get rid of duplicates. One reason dumping duplicate IP/wallpaper downloads is important is as I am using Android DownloadManager now, downloads now are more broken up and - duplicated. I solve this problem by creating a unique set, and seeing if unique data structures are in that set or not. Any how, now it takes five seconds to process the 145,000 downloads I have, whereas beforehand it took a few minutes. I had identified the problem of uniqueness beforehand, but surprisingly it took me less than an hour to solve the problem.
Back on the Android client side, and looking at the small, low density emulator, I see that some foreign languages use fonts which are too large for the download and set wallpaper buttons. So I shrink the font sizes accordingly.
I sent out to do translations for the app, its Google Play blurb, its ads, as well as the descriptions of a few of the more popular wallpapers. The app is already in English, Spanish, German and French, I am now doing Czech, Russian, Polish, Portuguese, Korean, Italian and Dutch. Those seven languages were determined from two factors - one, the number of images in Wikimedia Commons that were in those languages, and two, the amount of ad revenue which I could generate in those countries. If the cost of a translation and a small ad campaign could be recouped within a certain time period, then I opted to choose that language. There aren't many Korean language images in Wikimedia commons, but there is so much ad revenue in Korea that I paid the $30 to translate it any how. Insofar as an ad campaign there and if I'll have enough images in that language to fit the bill, I'll deal with that when it comes up. My app's multi-language capabilities are already superior to that of some of the leading wallpaper apps.
There is not a lot to do now. The refactored version has been out for over two months and all the major bugs have been fixed, except for a few infrequent and hard to track down ones. I'll just try to keep adding three or so new wallpapers every day, as I have been doing. This will give more of a selection, and fill out the categories more. Once I reach some threshold with the wallpapers, I will put in search functionality so that people can search for the wallpaper they are looking for. That would be the next big change for the app.
So, now that necessary upkeep on this app has dwindled (hopefully) to a few hours a week for the foreseeable future, I'll start pulling out some of the other irons I have on the fire...