Automated Detection of Common APK Issues
Part of my day-to-day tasks at Teak.io is developer support.
I have seen some of the same issues show up again, and again on a monthly (if not weekly) basis.
After writing some scripts to allow me to address support requests more quickly, I decided the ideal application would be to make a tool that could be integrated into build systems as a kind of “unit tests for build output.”
Introducing ‘headdesk’
headdesk is a Ruby Gem which unpacks an Android app (APK), processes the contents, and then runs checks on it.
Each check is like a unit test for the output of your build system. It will pass, fail, or be skipped. It will also export information relevant to that check so that you can easily see the values it was looking at, and even do further processing.
Each time I get a support request, I now write a check that would catch it.
Feature Requests
The current battery of checks is based on my support requests for Teak.
If headdesk doesn’t do what you need it to do, make an issue on GitHub and I’ll see what I can do!
We Need Tools
After a few years of doing this, let’s just say I’ve seen things…
One of the biggest sources of issues, across all projects that I’ve seen, is build configuration management. It is very easy to neglect a build system, and just as easy to ‘lose’ how it works.
Blindly-trusting the output of a build process will bite you in the ass. You need to make sure that you are shipping the thing you think you are shipping.
I’ve seen builds get shipped that crash with references to SDKs that were removed. Release builds with debug credentials, or the Facebook App Id of a different game.
We need tools that will automate checking for these issues.
Use Cases
There are three major categories where headdesk will help your build process.
Build Configuration Validation
Mobile games use a crapload of SDKs, each of which wants credentials. Then there’s url schemes, configuration files, and all of this is compounded by having your dev, QA, and release versions.
It’s important to know that each environment has the proper configuration, and the only way to do this is to verify the final product.
headdesk will export the data which it analyzes as JSON. One of the built in checks is for GCM and Teak configuration.
{
"teak_app_id": "1234567890123456",
"teak_api_key": "abcdef1234567890abcdef1234567890",
"gcm_sender_id": "123456789012"
}
You can test these exported values to make sure they are correct for your dev/QA/prod build.
Early Deprecation Warning
Old SDKs, Google Play Store requirements, unsupported OS versions are all pain points that every game I have worked with has felt.
Late last year (Nov. 2018), Google said all Google Play apps needed to target API 26. This caught teams by surprise.
Facebook only guarantees that their APIs and SDKs will work for two years. This is easy to forget.
Google is deprecating the GCM libraries in favor of FCM, and you need to update by April 11th.
headdesk checks for these cases, and warns you. This lets you make plans to update your SDKs, instead of being a surprise a week before a release.
Common Crash Detection
In the chaos of updating multiple SDKs, or switching from one vendor to a better vendor, simple things get overlooked.
Runtime dependencies can get removed, and cause crashes with seemingly mysterious reasons.
headdesk detects some of these cases and warns you.
Let’s Ship Better Games
No automated tool can catch all issues, but let’s stop working without a safety-net.
headdesk will help you ship better games.
I will help you, whether you are my customer or not.
Let’s ship better games, together as an industry.
Technical Details
The remainder of this post will be focused on the technical aspects of the tool.
Usage: Command Line
On the command line, headdesk will output nicely colored and formatted output, and you can ⌘ + double-click
on the URL to open the docs for a check.
It can also output JSON, and that’s where you can read status, flag a build as failing, or use the exported data (more on this later) to do further analysis.
See the docs for full usage instructions.
Usage: Hosted
I’ve also made it available as a web API, so you can use it from anywhere with a simple cURL command.
curl --silent -w "%{url_effective}" --upload-file "PATH_TO_YOUR_APK_FILE" \
-L "`curl --silent https://headdesk.cli-apps.teak.io/v1/url`" | \
curl --silent --data @- --retry 10 https://headdesk.cli-apps.teak.io/v1/analyze
The hosted version will return JSON.
Writing Checks
I wanted writing a check to be like writing Rspec. As such there’s a handy DSL which makes it easy to access the decompiled bytecode, the resources, and the AndroidManifest.xml of an APK.
My goal was that any time I got a support request, I’d write a check to automate finding the issue.
Docs
I took inspiration from a Ruby tool called reek and so each check includes a documentation page which describes the reason the check exists, what causes it to fail, if it will be skipped, and what it will export.
Writing Checks
A very common issue that I’ve seen in the , “Why does my app crash?” department is: the developer changed their build configuration, or removed an SDK without removing the <receiver>
in the AndroidManifest.xml.
When an event is received that is picked up by that receiver, the app crashes.
The receiver check detects these cases. It will iterate over all <receiver>
blocks in the AndroidManifest.xml, gets their ‘name’ property (which specifies the Java class that will be instantiated) and then makes sure that that class is present in the code.
# frozen_string_literal: true
module Headdesk
module Checks
#
# Make sure all <reciever> blocks in AndroidManifest.xml point to a Java class
# that exists in the APK.
#
class Receiver
include Check::APK
check_name 'receiver'
describe 'All <receiver> blocks in AndroidManifest.xml point to valid Java classes'
def call
receivers = []
apk.android_manifest.xpath('//receiver').each do |receiver|
receiver_name = receiver.attributes['name'].to_s
fail_check unless: -> { apk.class?(receiver_name) }
klass = apk.find_class(receiver_name)
describe "#{receiver_name} has onReceive method"
fail_check unless: -> { klass.method?('onReceive') }
receivers << {
name: receiver_name
}
end
export receivers: receivers
end
end
end
end
Accessing Resources
From a check, you can easily access Android resources that have been parsed and put into an object which uses method_missing to let you sloppy-access whatever you are looking for.
You can specify an API version as well, to examine values in the same way that the app will see them on different Android versions.
icon_v21 = apk.resources
.values(v: 21)
.drawable.io_teak_small_notification_icon
As of this blog post, the resource types supported are: string
, integer
, color
, bool
and drawable
.
Decompiled Java
Checks can easily access decompiled Java code to pull out constant values, validate library support, or whatever else you can do with bytecode (I’ve…done some things).
Here is some code which is used by the facebook check, to read out the version of the Facebook SDK.
facebook_sdk = apk.find_class('com/facebook/FacebookSdk')
get_sdk_version = facebook_sdk.method('getSdkVersion').code
major, minor, patch = get_sdk_version.match(/const-string v0, "(\d+)\.(\d+)\.(\d+)"/)
.captures.map(&:to_i)
This uses find_class
and method
to get the bytecode for the ‘getSdkVersion’ method of the Facebook SDK, and then parses out the constant containing the SDK version with a regular expression.
Exports
Checks don’t just pass or fail, they will also export the data that will help you debug problems, or use for your own testing.
This is example output from the facebook check, which will check the version of the Facebook SDK you are using, and make sure it was released within the past two years (which is Facebook’s deprecation cycle):
{
"facebook_sdk": {
"major": 4,
"minor": 33,
"patch": 0,
"version": "4.33.0",
"date": "2018-05-01"
}
}
Source Code
headdesk is MIT Licensed, and available on GitHub: https://github.com/GoCarrot/headdesk