TalentLMS for Android has been out for a while now and its user base is growing steadily. Following the example of the popular TalentLMS web-based platform, it offers a rich-content learning experience, optimized for Android devices. Version 2 of the app was released on Google Play a few of weeks ago, adding new features and multiple improvements to the previous version.
TalentLMS for Android
The most important of these features is deceptively subtle and perhaps not immediately noticeable to most users. In fact, if its implementation is as good as we would like to think, it should go unnoticed by everyone.
The internal name for this feature is the Content Downloader and, as the name suggests, it makes sure that content from the user’s Courses is always made available on the mobile device (for both online and offline consumption when possible) as soon as the user looks for it.
Despite it being planned as a core feature during the app’s conception stage, its complex nature only allowed us to ship it months later, in version 2 – teaching our development team a few lessons along the way.
TalentLMS’ content structure
If you have used TalentLMS before, you might know that content is organised in Units, which are in turn grouped in Courses. You could think of Courses as books and Units as chapters in each book.
Each Unit is characterized by a type that depends on the content it contains, such as Audio, Video, Text, Test, Survey, and others. TalentLMS for Android does not yet support all of the Unit types available in the web version of TalentLMS, but we are gradually getting there.
Some Unit / content types, such as uploaded video, audio and PDF files, can be made available offline, while others, such as Youtube or Vimeo videos, cannot. If a Unit can be made available offline, it is the Content Downloader’s job to download its metadata and content as soon as the user expresses interest in it, via their actions on the UI.
When we first started designing the TalentLMS mobile app, we knew that enabling our users to have access to their content on all their devices, even when they were offline, was of utmost importance. While developing the architecture of the Android app, we set a few goals that would make content consumption, and the intermediate downloading process, as seamless and transparent to the end user as possible.
Below is an overview of those goals:
Short waiting times
All screens must be populated with useful content as soon as possible. Content downloading should happen automatically and progress indicators should be a last resort!
Users must have offline access to as many Units as possible, provided that they have indicated interest in those Units via their use of the app.
Redundancy and error handling
Large content must be downloaded as soon as possible, no matter how many errors occur due to poor, intermittent network connections or other unexpected events.
Download stopping and resuming
No part of a file has to be downloaded more than once. Furthermore, downloads can be stopped and resumed either through user input or due to external factors (e.g. network or application state changes).
One-button Course download and queuing of content when a user expresses interest in it via their actions.
Realtime progress reporting
The user must always be informed of the current progress of all downloading content they encounter in a non-intrusive way.
Reduced data usage
Non-essential content is only downloaded when the device uses Wi-Fi, unless the user has indicated otherwise via the app settings.
Content downloading must not affect the overall perceived speed of the application.
Reduced storage space / Offline content management
Allow the user to selectively delete offline content to free up space.
Low battery consumption
All of the above must take place without abusing the network or other subsystems of the device.
When development began, the Content Downloader infrastructure and the rest of the app had to provide a complex set of features, in order to achieve all of the goals mentioned above. More specificaly, these features:
a) Efficient background function
The downloading of content should in no way interfere with the user’s actions. The app should remain quick and responsive at all times and the user should be able to resume the tasks they were performing before downloading was initiated.
b) Download queueing
Downloading many files at the same time is extremely inefficient, particularly on poor Wi-Fi or mobile data connections. The number of concurrent download slots should be limited depending on factors such as network quality and device capabilities. This should happen transparently, without making users to experience any effects of this limitation.
The user should be able to ask the app to download an entire Course, or even multiple Courses at any time, without having to think about how many files are actually being concurrently downloaded. In order to achieve that, the downloader has to maintain a queue of Units to be downloaded and the order of those Units should reflect their priority. Whenever a download slot is freed, the Unit at the top of the queue should begin downloading.
c) Stopping of active Downloads
At first, this feature might not seem very important, but in reality, it is one of the core ingredients for the successful implementation of the downloader! Without it, the download priorities of Units cannot be changed fast enough for a smooth user experience. Even if the downloads queue priorities are updated, they cannot be enforced before new download slots are available. New download slots will only become available once an active download finishes or fails.
The reason that content priorities might change is that the user is continuously navigating the app. Due to that fact, old content that is currently being downloaded can rapidly become less important than new content that is being requested by the user right now.
Furthermore, even if download slots are available and priorities do change, the only way to ensure that high-priority downloads are completed quickly enough, is to allocate more bandwidth to them, by stopping lower-priority downloads.
You can read more about the downloading strategies used in Part 2 of this series.
d) Download resuming
Partially downloaded files should not be deleted from the disk. Instead, when their turn to be downloaded comes again, they should be resumed using HTTP 1.1 Range requests to the server. Downloads should only be restarted from scratch if the server does not support Range requests, or if a request fails with a 416 (Range Not Satisfiable) HTTP status code.
e) Progress reporting
No matter how well prioritized the downloading of content is, it is very bad UX for users to be staring at endless spinners. Once a download has been started, the app should be able to provide constant progress updates to the UI so that the user knows what’s going on.
f) Failed download retrying
Downloads may fail for various reasons, particularly when they are taking place on a mobile device which could be situated anywhere, from a speeding car to an underground metro station. That, however, is nothing the user should be bothered about. If the failure is not due to a critical error, the downloader should try to resume and complete the download.
g) Error reporting
Whenever a download fails and cannot be resumed (for example, if there is no more space on the device or a file is no longer available on the server) the relevant exception should be propagated from the Content Downloader infrastructure to the higher layers of the app. If the user should be notified, the presentation layer will decide what to do.
h) Network state awareness
The app should be aware of the state of the network in order to adjust the Content Downloader settings as soon as a change occurs. For example, whenever the device switches to mobile data, the Content Downloader should stop downloading all non-essential content (unless the user has given explicit permission) to avoid unnecessary service charges.
Also, the Content Downloader should stop trying to resume failed downloads whenever the internet connection is offline, to save battery.
i) Save content state in persistent storage.
The status of each Unit’s download progress should be saved in persistent storage as often as possible, so that its state can be maintained, even in cases when the app is stopped unexpectedly. This way, no additional network requests have to be made before the user is notified about the actual state of their content on the device.
j) Post-processing for certain types of content
Since file decompression was a requirement for the upcoming SCORM functionality of the app, we decided to add a post-processing aspect to the Content Downloader, as well. Once the content has finished downloading to the device, an extra post-processing step should take place, for content types that require it.
Although it was added for decompression, we decided it should be easily extensible to support all kinds of time-consuming processing that should happen in the background and would benefit from features such as progress and error reporting and state preservation.
Although the implementation of all the above features was more time-demanding than we had initially anticipated, we managed to achieve all the goals we had set in the initial design and keep a clean and extensible codebase. We were, of course, stepping on the shoulders of giants, since a lot of open-source projects helped make our lives much easier, by simplifying the code required to solve complex projects.
You can read more about the technologies used in Part 3 of this series, but first, you might want to know more about how the downloader component was integrated into the existing application flow in Part 2.
In Place Of A Cliffhanger
This is part one, a more general overview of this new feature. It may seem quite technical but there’s a good reason for that: by design, you wouldn’t have noticed it! Stop by for Part 2 of this discussion on the Content Downloader, its capabilities and the tech that makes it possible.
About the author: Manolis is currently obsessing over the architecture and development of the team’s mobile applications. He has been talking to computers for the better part of his life and has (ab)used all kinds of technologies, ranging from 68K asm to JS.