Previously in this series we’ve built a Wear app designed to time football matches. While we finished the coding in the last article, there is still another important thing to do if we want to distribute our app via Google Play, and that’s package it up! In this concluding article of this series we’ll look at how we do this, and also discover a pitfall on the way.
Wear devices do not have Google Play on them, so there is no way to directly install apps on to them. However, it is no great problem because many Wear apps are likely to be coupled with apps running on the phone or tablet with which the Wear device is paired. In such cases the Wear app gets bundled in to the phone / tablet APK and it will be automatically installed on the Wear device. Match Timer is a completely standalone app (at least for now as that may change with a future series of articles), so how do we install it? Actually we do exactly the same thing: We package it inside a shell APK which gets installed to the paired phone / tablet, and the Wear component will be automatically installed on to a paired Wear device.
There’s a full description of how to do this on the Android Developers website, so I’m not going to simply duplicate the information listed there. However, when packaging Match Timer for distribution I encountered an issue which caused me a few problems.
The issue was with the mobile app that I created as the shell. As I’ve already mentioned, this is just an empty shell APK which is used solely as a transport / installation vessel for the Wear APK. It contains no code, and just a few resources to support a simple manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stylingandroid.matchtimer"> <application android:allowBackup="true" android:label="@string/app_name" android:icon="@drawable/ic_launcher" android:theme="@style/AppTheme"> </application> </manifest>
The problem that I encountered was simply that the Wear APK was not being installed to my Wear device. It took quite a lot of figuring out to work out what was going on here. Eventually I checked logcat on the Wear device and discovered that it was a permissions issue – the Wear device was refusing to install the Wear APK because it required permissions that were not being declared by its parent APK. This actually makes perfect sense when you think about it – the inner APK required permissions which weren’t being declared by the outer APK and it could be a huge security flaw to permit permission to the inner APK which the user had no visibility of. So, a simple tweak to the manifest of the outer APK fixed this issue:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stylingandroid.matchtimer"> <uses-permission android:name="android.permission.VIBRATE" /> <application android:allowBackup="true" android:label="@string/app_name" android:icon="@drawable/ic_launcher" android:theme="@style/AppTheme"> </application> </manifest>
The mobile APK now matches the permissions required by the weak APK and everything now installs correctly.
Just before we finish, it is worth explaining another small part of the project build configuration which may be of interest to those running a Continuous Integration server. I have been testing my project using a Jenkins CI server and ran in to a small problem. I wanted to be able to publish the sources to the project without having to include my signing key as that you be a bit stupid!
I explored a couple of approaches: First I tried a local.properties
file which contained the signing key information. This worked fine on my local development machine, but didn’t work on the CI server because it was doing a clean checkout of the code for each build, and there was no local.properties
file on the CI following a clean checkout.
Next I tried setting these values using environment variables. This worked fine on the Jenkins CI where I was able to install a plugin which would inject environment variables at build time. However this broke my local build because the Android Studio build environment did not include the environment variables.
In the end I decided upon a hybrid of the two approaches:
def loadSigning() { if (project.rootProject.file('local.properties').exists()) { return loadSigningFromLocalProperties() } else { return loadSigningFromEnvironment() } } def loadSigningFromEnvironment() { Properties localProperties = new Properties() localProperties.setProperty('ANDROID_KEY_STORE', System.getenv("ANDROID_KEY_STORE")) localProperties.setProperty('ANDROID_KEY_STORE_PASSWORD', System.getenv("ANDROID_KEY_STORE_PASSWORD")) localProperties.setProperty('ANDROID_KEY_ALIAS', System.getenv("ANDROID_KEY_ALIAS")) localProperties.setProperty('ANDROID_KEY_PASSWORD', System.getenv("ANDROID_KEY_PASSWORD")) return localProperties } def loadSigningFromLocalProperties() { Properties localProperties = new Properties() localProperties.load(project.rootProject.file('local.properties').newDataInputStream()) return localProperties; }
It looks for a local.properties
file and, if one exists it uses that. If no local.properties
file exists, then it attempts to load them from environment variables instead.
In this way I was able to achieve my target of a build which did not include signing keys, yet would build on both my local Android Studio and my Jenkins CI server.
That concludes our series on Android Wear.
Match Timer is available on Google Play.
The source code for this series is available here.
© 2014, Mark Allison. All rights reserved.
Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.