Android apps.........................
Follow a no-nonsense Mats Tage Axelsson’s example, as he shows how you can ensure your mobile apps work where you want them to work.
Mats Tage Axelsson fires up AndroidStudio to help your build Android apps that will run on every device you could possibly imagine.
AndroidStudio gives you plenty of options, but to choose the correct platform you may need to tweak your environment. The default setting is API 21 that corresponds to Android 5.0 (Lollipop), which means most devices are compatible. If you want more features, step up to a newer system, but be prepared to lose users on older devices.
Developers want to create an application for the masses, collect the winnings and then retire to a tropical beach sipping a mai tai. But in the real world, we have projects that need to be designed for a range of older devices. In fact, when we make our own application we may want to reuse the old device for unconventional purposes.
How do we make sure our software will work on the specific model? Well, first, we need to make a plan that shows what the application needs to do. For example, it might a project that needs some controller, or you want to make a remote controller out of your old Android phone.
Next, determine what hardware you have available and need to use. Are you going to use the accelerometer or camera? You may even want to utilise features that are in a newer version or even on an older phone. This is possible to a degree, using the Support Library Features library.
Before we start coding, we need to create virtual devices. Look at your plan to make sure what API level you’re interested in before you continue creating a virtual device. You can do this with the AVD manager in AndroidStudio. The procedure has been covered in LinuxFormat before, but there are a few details we need to cover.
As mentioned earlier the default API level is 21, which is a little limiting for our purposes. Things get a little confusing when installing older API levels because when you run the manager, the devices are shown first without the level they’re on. To handle this you can pick any device and move on to the next step where you can choose the API level. Bear in mind that most images which support older levels are not in the default install, so you need to download the older levels separately. The level can also be changed after you’ve created the virtual device. Make sure you pick images for x86, because this will create the quickest emulation.
Stay in control
The AndroidManifest.xml file controls what your application can do and will support. In this file, you have all the permissions and requirements for the application you’re building. When working in AndroidStudio though, the file looks suspiciously small, so if you can’t see your settings in the file you can click the Merged tab to show the combined version. The reason for the merged tab is that many of the settings are created in gradle files and are merged during the compilation process. For our purposes, the important parts in the manifest are <uses-feature> and <uses-sdk>. The AppCompat library will also play an important role.
A commercial developer will want to make their product available to all Android users. Yet adding newer features will reduce the product’s user base. There are several ways to find your perfect balance for your application. First, make your AndroidManifest file reflect a wide range of API LEVELS.
In your application, you may want to use a camera, but as an optional feature. If this is the case, make sure to declare the camera with the required parameter as false. This way users can use a subset of features if they lack the hardware: <uses-feature android:name="android.hardware.camera.any” android:required="false” />
One commercial example of this is Evernote. You can add pictures using the camera but if you don’t have one, it works fine anyway. If this feature had been set to true, it would not be possible to install the app on devices without a camera.
Put “targetSdkVersion” and “minSdkVersion” in the code of the AndroidManifest.xml file to create the range of levels
that you’re seeking:
<uses-sdk android:minSdkVersion=”15” android:targetSdkVersion=”26” />
You need to be aware that if you use gradle, then this is created in the build.gradle file for the module app in another format. The information is merged during the compilation stage into androidManifest.xml. You can view this in Android
Studio using the Merged tab. The parser will then allow all features in the range and Play will show the application only to users who have a compatible device. When you run the application on an old model in the range, a problem may occur: a feature that’s not supported may be started but will fail to execute. To stop this from happening, you need to set a runtime check that figures out what to do when the feature is missing. The feature can be dropped or replaced during runtime − you’re responsible for the code that decides this. The code below handles a new behaviour regarding theme handling in API 23 and higher, where the phone will no-op if you initiate a themeID that’s identical to the existing one.
if (Build.VERSION.SDK_INT >= 23; {
onApplyThemeResource(getTheme(), mThemeId, first: false; }else{ setThemeId(mThemeID); }
The class can be called with both the integer, as shown above, and constants with names like DONUT, ECLAIR and GINGERBREAD. There’s also a magic version number that you use when you develop for an upcoming release; this will never be used for a production version of your code.
When you want your code to run on a rooted device with derivatives, you may run into problems because the version is ambiguous in this case. However, most code will run anyway.
Using the AppCompat library, you can use new features on old systems. One such feature is the Toolbar, which was introduced in API level 21. This feature is possible to use all the way back to API level 7, thanks to the AppCompat library. When using it, you need to go through all the code that utilises it. This may seem easy, and it is… but it’s also easy to get lost in all the parts where the code uses the library.
Let’s say you want to use the new toolbar. To do this you need to define it in several places. The first part is to declare that you want to use the library in your application. In your
MainActivity file, you need to change the import statement from android.widget.Toolbar to add the support library. You need to add each component of the support library: in this case, we want to use the toolbar and the activity library:
import android.support.v7.widget.Toolbar import android.support.v7.app.AppCompatActivity
This code only makes the library available to the application. To use it you also need to update your classes. We have a small application so we update the MainActivity class in the corresponding file. When your project increases in size you’ll have to brush up on your search skills.
class MainActivity extends AppCompatActivity { ... }
In the same file, since this is a small application, we’ll find calls to setActionBar(), which calls the old libraries and so need to be changed. If you have a bigger application to update, search all your files for this specific method. setActionBar(toolbar)
/change to setSupportActionBar(toolbar) /and actionBar? /change to setSupportActionBar?
If you’re using the fragment manager, you need to update these call, too. The reason for fragments is to call activities from within another activity and to go through the entire life cycle without affecting the main activity. In systems where you don’t support levels higher than API 11, you can use a regular activity call: fragmentManager()
/change to supportFragmentManager()
One reason to use the Fragment library is to bring up a pop-up window that pauses the main activity it starts from. When the fragment is running the main activity will be in paused mode. Usually, this means all parameters and states are preserved. An exception occurs during extreme low memory situations: the system can kill the process or ask it to finish. To prepare for that event you need use an onPause() method to store any critical data before opening a fragment. The onPause method must be quick because the next activity must wait for the onPause method before starting. The data can then be retrieved by the onRetrieve() method.
When all this is done you still have some work to do for your resources and layouts. The update is necessary because the layout code calls the appCompat library. In the folder is
styles.xml − this file contains the theme you’re using. Set the value to include the appCompat as per the code below: <resources>
<!-- Base application theme. --> <style name="AppTheme” parent="Base.V7.Theme. AppCompat.Light"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</ item>
<item name="colorPrimaryDark">@color/ colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> </style> </resources> When running AndroidStudio you can always use the theme editor if you prefer a graphical view.
Memory usage
The general recommendation is that you create an APK that supports all hardware you can think of. Sometimes though, this leads to a very big APK and since we’re trying to use old devices, chances are that they also have memory constraints. So each byte counts. Fortunately, you can use filters that enables publishing multiple APK for the same application. The filters you can use are those mentioned earlier: <uses-sdk>, <supports-screens> or <compatible-screens> and <supports-gl-texture>. You use the last one to set a specific texture for your graphics. Each definition requires one texture, and this texture is supported in hardware or as a file. The file can be downloaded at runtime, be embedded in the APK or already supported by the device. For example, the ‘GL_OES_ compressed_ETC1_RGB8_texture’ is available on all Android devices that support OpenGL ES 2.0, so if you have an old Ericsson phone the graphics will be handled already. Some other devices are equipped with a GPU from the likes of Adreno or Nvidia though, and in this case, the textures are in the GPU. With that hardware, the memory requirements will be even lower.
Non-standard CPUs
To handle rarer CPU models, your best bet is to use the Native Development Kit (NDK). You need to be a little more experienced in coding because it actually makes C++ available for you. If you scour around the internet though, you can find many exciting libraries for animation, machine learning and gaming. In our case, we want to find a way to make one APK for each architecture that we target.
First, remember that this is only when you can’t use Java to create your software, since the result will be exclusively for this platform. The maintenance of the software will be more complex and time-consuming. However, this is the technique to use when performance is key. And quite frankly, we like the idea of having an alternative approach at our disposal.
In your project, you need to create a “splits” block to tell Gradle to create one APK per CPU that you’re targeting. The example code below sets the compilation to create a separate APK for x86:
android{ splits{ abi{ //Enables building APKs per ABI enable true //Reset the list of ABIs before you start reset() //Specify a list of ABIs that you need include “x86”, “armeabi-v7a” //Decide if you want a universal APK also universalApk false } } }
When using this Gradle, make sure you have two APKs: one for your x86 and another for armeabi-v7a. The x86 version will then be smaller and hopefully faster than a
universal APK would be. Performance benefits will vary depending on what you’re demanding from your application. Heavy image manipulation can benefit greatly from native code, and up to 20 times improvement is possible. When other applications are tested, the difference can be negligible when your code doesn’t use the CPU or GPU heavily.
The performance difference between 64-bit and 32-bit are around five to 10 per cent according to Intel’s own testing. This means that when you choose between the two you can usually skip 64-bit. Why? Because the 32-bit will run on all 64-bit versions. However, when you compile for 64-bit, you also have to include the 32-bit native code. So there’s little benefit to be had by adding the 64-bit element, and it’s a bigger package, too. Obviously, if you’re more into performance and have the space, use the 64-bit APK.
Upgrading your work
Creating several APKs for one application isn’t recommended because you have to update all versions when you fix something or add a feature. There are other reasons for not recommending many APKs in one application, too. When an application is upgraded, it must be increased from the earlier revision. It must also be unique on the Play Store, so you can’t use v1 for both x86 and armeabi. This leads to a strange situation where the newest version may be the ABI that you just changed, but this isn’t really related to anything.
To solve this problem, versionCode has been introduced, which differs to the versionName that you find in the AndroidManifest.xml file. versionCode is an eight-digit code that ends with the version name. For example, you can set the first digit to the ABI and set the last digits to the version.
Using this method, you can also handle another quirk with the system. If a device supports two ABIs, then it’ll receive the highest revision of the two. If you don’t set the most important ABI to the highest value every time then it’ll keep changing for that device. The reason this can happen is that many devices can run both armeabi and the newer armeabiv7a. Because you have eight digits you can also set other values in this code, such as the API level and the screen size.
To set these values you declare it in the build.gradle file. The code is calculated by the script below. Note that the version includes nine architectures, even though we only create three APKs. This makes it simpler to handle new architectures should you decide to add them: project.ext.versionCodes = ['armeabi': 1, ‘armeabi-v7a': 2, ‘arm64-v8a': 3, ‘mips': 5, ‘mips64': 6, ‘x86': 8, ‘x86_64': 9]
android.applicationVariants.all { variant -> variant.outputs.each { output -> output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter( com.android.build.OutputFile.ABI), 0) * 10000000 + android.defaultConfig.versionCode } }
Once you’ve compiled your APKs, you need a way to confirm that the result matches what you hoped for. You can find the AndroidAssetPackingTool (aapt) in your SDK folder under ../sdk/build-tools/ or install it using your standard packaging tool. Execute the aapt command as below. $ aapt dump badging AndroidApp.apk
Search for ‘versionCode’ and ‘native-code’ in the printout. Bring out your scripting ninja because the printout is very big. The dump can be used to verify that you have the correct locales and a multitude of other aspects of your APK.
This article has assumed that you’ll either load your application directly to your device or use the Play Store to distribute it. If you expect to use other stores to distribute it you may have a problem with the versioning, and users may download an incompatible APK for their device.
An advanced but small library is also available to detect what CPU and which features are available on your device. The library is called ‘cpufeatures’, unsurprisingly. You can include it in your compiling process by adding a call to the library at the end of the Android.mk file: ... $(call import-module,android/cpufeatures)
There are only two functions available in the library but if you need them they are essential. The two functions are android_getCpuFamily(); and android_getCpuFeatures(); The first function returns the CPU family as an enum or 0 if the CPU isn’t supported. The second function will return different values depending on the CPU family it comes from.
In ARM there are many instruction sets that will help you distinguish between them. Under the x86 family you can even find the number of cores that are available on the device.