In BC, students of a bunch of public post-secondary institutions get access to a special transit pass, called a UPass. Over the past year or so, Translink has upgraded the standard fare system across its network to the Compass Card, a reloadable card with stored value that replaces paper tickets and monthly passes. The UPass got an upgrade too, and is now stored on a student's Compass Card. Renewing the UPass is now done online, through the UPassBC website.
Let me give you a quick rundown of the UPassBC system. The login process is as follows:
Selecting your school redirects you to your school's website, where you use your school account to authenticate, and are then redirected back to the UPassBC website to renew.
The inspiration for RenewPass was a script written by Alireza Alidousti. It's called upassbc, and it renews your UPass by automating the three steps outlined above (select school, login, renew). You give it your credentials, it gives you next month's UPass.
The script is everything I didn't realize I wanted. Because it's just a script, I can run it on my laptop/server through a cron job, and automatic renewal is taken care of. Additionally, the ability to run it locally adds peace of mind, since I don't need to share my school credentials with anyone else.
But I realized after using the script that, while a perfect solution for a CS student, it wasn't a very good solution for the majority of students. Most of the people I know don't use the command line, and the script doesn't give much feedback to the user, so even if I did help others set it up they wouldn't necessarily be able to easily know if the script worked without logging in to UPassBC to check and making the script's actions redundant.
My idea was to take the upassbc script and wrap it in an Android app. I wanted to keep the simplicity and security of the script, but use a medium accessible to far more students. The app would have a dead simple user interface, and I would store the credentials on the device so the user didn't feel concerned about handing out their school information. I could even improve upon the original by adding support for more schools (upassbc only supports SFU) and building in automatic renewal.
And just like that, RenewPass was born.
The first order of business was translating the upassbc script from Ruby to Java. This was also my first mistake.
I used a library called Mechanize for Java, which is a port of the Ruby Mechanize library used in the upassbc script. My thinking was that by using a library that is the Java equivalent to the library used in the original script, I didn't have to worry about the little complexities in the script, and could just translate it nearly verbatim. This would save time as well as effort in making sure the script worked.
My theory was mostly correct. Translating the script was pretty trivial, and I organized the pieces in a more object-oriented fashion. Specifically, I made sure the login part was done in a separate class than the rest of the scripting so I could easily substitute in other schools.
While the code does work, and I did save quite a bit of time and effort in trying to do it from scratch, the Mechanize for Java library isn't well supported. The issue with not being well supported is that maintenance and improvements are more difficult, because solving errors becomes significantly more challenging without any Stack Overflow posts to help you. I've had Software Engineering courses that taught me that maintenance is a very large part of software development, but I didn't realize how big until this decision came to bite me in the butt. One of the big improvements I plan on making is a large refactor of the scripting using a new library.
As mentioned, one of the main reasons I wanted to build an Android app was to make an interface to the upassbc script that was more accessible than the command line. I'm a Material Design purist, so it was my mission from the get-go to make something simple and beautiful. The design that I settled on is about as simple as it gets: one button.
The renew button sits in the middle of the screen, and tapping it begins the renewal process complete with a loading spinner around the button. Once completed, a checkmark covers the button. In the event of a failure, the button is covered with a red x and an error message is displayed in a snackbar along the bottom edge of the screen, usually with a button allowing the user to take some action to fix the error (retry if it's a network error, change their username/password if it's an authentication error etc).
This design is one of the decisions I'm very happy with. It's stupid simple, and conveys all the information necessary to the user. The only other activity in the app (save the introduction slides) is the Settings activity, so I believe I've achieved maximum minimalism.
With the user interface done, I started thinking more about how to best secure the username and password of a user. I wanted to keep things local, so my hands were tied no matter what I do since there is a way to obtain the data if the device is rooted. Therefore, my strategy is just to try and make it as difficult as possible to obtain a user's credentials.
I settled on encrypting the password before saving it in Android's SharedPreferences, and storing the key pair in Android's KeyStore. This provides the advantage of requiring root access to the device to get access to the data, and forces the elite hacker to find the key used for encryption before being able to read the plaintext. Obviously, this data is all on the device, but creating an extra dependency and separating it from the SharedPreferences was the best solution given the constraints.
The first release of RenewPass was everything covered above, as well as the (mentioned) introductory slides which collect your school credentials. It covered the basic use case, and I wanted to get it out in to the world. But it wasn't complete without automatic renewal.
With the extensive help of my friend Trevor Clelland, an automatic renewal and notification system was added for version 2.0. Schedule a date and time, and RenewPass will renew your UPass and let you know the result.
To be completely honest, the code for automatic renewal is boring. It's simple BroadcastReceiver/AlarmManager code with some additional functions for dealing with the date/time. What's really interesting to me is how vulnerable I felt without automated tests. Actually testing the automatic renewal involved a lot of toast debug messages and scheduling alarms for a few minutes in the future, but even then I felt concerned that things wouldn't work with every new change. Part of the reason I was so paranoid is because automatic renewal has to be reliable, no questions asked. If automatic renewal doesn't fire at the right time, or fails to show a notification, the user has a worse experience than if they didn't use the app at all. People would rely on RenewPass to get the job done, so it had to work, and the experience of developing this feature has engrained the value of automated tests in to my mind.
I still haven't written any, but the paranoia of releasing without running tests is still fresh in my memory, so I plan on creating some soon.
One of the most surprising challenges I faced was trying to effectively troubleshoot problems people were having with RenewPass. I was naive in thinking that since the app worked in my tests (and tests run by my peers), it worked for everyone. As problems started being reported, I realized how difficult it was to troubleshoot the app when I can't reproduce the issue, and I can't access the other person's device. I really should have seen this coming.
My first improvement was to display more information in the error snackbar that appears when renewal fails. Specifically, make sure that all of the errors I was aware of had descriptive messages, and buttons when possible.
My second improvement, and the solution to the problem, was logging. I had some logs placed scarcely throughout the code, but the logs were no good unless I could read them. I used the RemoteLogger library, and added log statements in places where there was useful information (what page did we get to before failing, what's the stacktrace of the exception that was caught). The library includes a function to send the log via an Android Intent, so I added a setting to trigger the intent with an email address prefilled.
With the logs in the right places, and the ability to request logs from a user, I was set. Another trivial and obvious problem solved.
Developing RenewPass has been an extremely positive experience. As I've said, I learned about problems I never would have (but probably should have) forseen, and developed solutions to most of them. I'm very proud of the work I've done, and the improvements I've made both to the app and to my abilities and knowledge throughout this experience.
I want to thank Trevor Clelland for contributing so much to the project, and being a fantastic help with all of the problems I've faced. I also want to thank Aaryaman Girish for lending a helping hand and voice to help shape the decisions that made RenewPass what it is. And to the other two Coffeeboys (Evan Chisholm and Emre Erhan), you help in mysterious ways, but it does not go unnoticed.
A humongous thanks to Prayansh Srivastava and Eugene Shen for their pull requests! I'm amazed that you both cared enough to sacrifice your time to help make RenewPass a better app, and the help you provided is very much appreciated.
Finally, thank you to the testers of RenewPass: Alfred Sin, Akshiv Bansal, Tina Ly, Umesh Dinkar, Manny Sangha, Stuart Harvey, Jacob Zhou and James Kao. Getting RenewPass to work with any school other than SFU would have been quite literally impossible without your help.