Best practices for Android apps
The best practices discussed in this article improve compatibility between Citrix Endpoint Management and mobile apps for Android devices.
MDX app SDK and wrapping
If your app uses the MDX App SDK, then you must use the matching MDX Toolkit version for wrapping. A version mismatch between these two components might cause improper operation.
To prevent such a mismatch, wrap the app with an app type of Premium or General. That lets you deliver a pre-wrapped app. As a result, your customer won’t need to wrap the app, thus avoiding use of a mismatched MDX Toolkit. For details about wrapping apps, see Wrapping Android mobile apps.
Don’t block the main thread
You should not use blocking code when running on the main thread. This is a Google guideline, but it is even more crucial with Citrix Endpoint Management. Some actions may take more time in a managed app or may even block further thread execution.
Blocking code includes, but is not limited, to the following:
- File or database operations
- Network operations
To be clear, all app lifecycle methods, such as onCreate, run on the main thread.
Google provides a StrictMode API which can help detect blocking code. For details, see this blog post: https://android-developers.blogspot.com/2010/12/new-gingerbread-api-strictmode.html.
Write robust code
In particular, you should check return values or catch exceptions from framework APIs. While this is just a common programming best practice, it is especially important for managed apps.
Various APIs that you’d expect to always work will fail if Citrix Endpoint Management policies block the underlying functionality. Examples would include any of the capabilities described earlier:
- Networking APIs fail as if there is no network available.
- Sensor APIs, such as GPS and camera, return null or throw an exception.
- Intents directed at a non-managed app fail.
- File and database access might fail if used from the main thread. For details, see the Ensure Data Encryption Compatibility and Encryption User Entropy sections, later in this article.
When you encounter a failure, your app should handle the issue gracefully instead of crashing.
Hooking limitations
MDX injects functionality into a binary Android app by modifying the DEX code in the APK. Several limits are present:
- Citrix Endpoint Management might not manage deprecated framework classes from the pre-4.0 Android SDK versions. Be sure to avoid those deprecated classes.
- Most functionality is injected into the Java/Android framework APIs. Native (C/C++) code is generally not managed. One exception is that even for native code, file encryption still occurs.
- Native code that uses JNI to access Java functionality must only target code in the user app. In other words, don’t use JNI to directly invoke Java or Android framework methods. Instead, use the proxy design pattern to “wrap” the desired framework class in a Java class of your own. Then invoke your class from the native code.
Ensure data encryption compatibility
One of the primary features of MDX is that all persisted data is transparently encrypted. You don’t need to modify your app to gain this functionality and, in fact, you can’t directly avoid it. The administrator has the ability to disable encryption either selectively or entirely, but the app does not.
This is one of the more heavyweight aspects of MDX and requires an understanding of the following points:
-
File encryption is present for all Java and native code that runs in managed processes.
-
Some framework APIs, such as media players and printing support, actually run in separate OS processes. If you use such an API, you might encounter issues.
- Example: Your app saves a file to disk (encrypted) and then passes a reference to the file to a media API. The media API tries to read the file but it doesn’t understand the encrypted content. It fails or even crashes the app.
- Example: You create a file handle (that starts an encrypted file) and give it to the camera API. The camera process directly writes unencrypted data into the encrypted file. When your app tries to read that data, the data is decrypted, yielding garbage.
-
One method of handling separate processes is to decrypt a file before handing it to the relevant API. Or if the API writes data, then you’d let it write first and then you’d encrypt it when the API finished. A few steps are required:
- Designate an area that will remain unencrypted. You must document this for your customer, because a Citrix Endpoint Management administrator must create an encryption exclusion policy.
- To decrypt, you simply copy the file from the normal (encrypted) location to the decrypted location. Note that you must do a byte copy and not a file move operation.
- To encrypt, reverse the direction. Copy from unencrypted to encrypted locations.
- Delete the unencrypted file when no longer needed.
-
Memory mapping is not supported for encrypted files. If you call an API that does memory mapping, it will fail. You should handle the error. If at all possible, avoid direct and indirect use of memory mapping. One notable case of indirect use is the third-party SqlCipher library.
If you can’t avoid memory mapping, the administrator must specify an encryption exclusion policy that omits the relevant files. You must document this policy for your customer.
-
Encryption adds measurable overhead. Be sure to optimize file I/O to prevent performance degradation. As an example, if you are repeatedly reading and writing the same information, you might want to implement an app level cache.
-
Databases are just files and so they are also encrypted. Performance can be an issue here too. The standard database cache size is 2000 pages or 8 megabytes. If your database is large, you might increase this size.
SQLite WAL mode is not supported due to the memory mapping limitation.
Encryption user entropy
One Citrix Endpoint Management option for encryption requires the end user to enter a PIN before the encryption key can be generated. This option is called user entropy. It can cause a particular issue for apps.
Specifically, no file or database access can be performed until the user enters a PIN. If such an I/O operation is present in a location that runs before the PIN UI can be displayed, it will always fail. There are a few implications:
- Keep file and database operations off the main thread. For example, an attempt to read a file from the app object’s onCreate() method will always fail.
- Background operations, such as services or content providers, may run even though no app activity is present. These background components can’t display the PIN UI and therefore they can’t perform file or database access. Note that once an activity runs in the app, the background operations are allowed to perform I/O operations.
There are several failure mechanisms if the encryption key isn’t available due to user entropy:
- If the main thread accesses a database before the PIN is available, the app is killed.
- If a non-main thread accesses a database before the PIN is available, that thread is blocked until the PIN is entered.
- For non database access started before the PIN is available, the open operation will fail. At the C level, an EACCES error is returned. In Java, an exception is thrown.
To ensure that this issue isn’t present in your app, test with user entropy enabled. The Citrix Endpoint Management client property, Encrypt secrets using Passcode, adds user entropy. You configure that client property, which is disabled by default, in the Citrix Endpoint Management console under Configure > Settings > More > Client Properties.
Networking and micro VPN
Several Citrix Endpoint Management policy options are available to administrators for networking. The Network access policy prevents, permits or redirects app network activity.
Important:
The MDX Toolkit version 18.12.0 release included new policies that combined or replaced older policies. The Network Access policy combines Network access, Preferred VPN mode, and Permit VPN mode switching. The Exclusion list policy replaces Split tunnel exclusion list. The micro VPN session required policy replaces Online session required. For details, see What’s new in earlier releases.
The options are as follows:
- Use Previous Settings: Defaults to the values you had set in the earlier policies. If you change this option, you shouldn’t revert to Use Previous Settings. Also note that changes to the new policies do not take effect until the user upgrades the app to version 18.12.0 or later.
- Blocked: Networking APIs used by your app will fail. Per the previous guideline, you should gracefully handle such a failure.
- Unrestricted: All network calls go directly and are not tunneled.
- Tunneled - Full VPN: All traffic from the managed app tunnels through Citrix Gateway.
Limitation: Citrix Endpoint Management doesn’t support socket server. If a socket server is running inside the wrapped app, the network traffic to the socket server is not tunneled through Citrix Gateway.
Mobile app development frameworks support
Some app frameworks have compatibility issues with Citrix Endpoint Management:
- With PhoneGap, the location service is not blocked.
- SQLCipher doesn’t work with encryption because it uses memory mapping. One solution is to not use SQLCipher. A second solution is to exclude the database file from encryption using an encryption exclusion policy. A Citrix Endpoint Management administrator must configure the policy in the Citrix Endpoint Management console.
Debugging tips
When debugging a wrapped app, consider these tips.
- Determine if the issue is present in an unwrapped version of the app. If the issue occurs when unwrapped, use normal debugging techniques.
- Try turning off various Citrix Endpoint Management policies.
- This can help localize any incompatibility. Disabling a policy means that MDX no longer enforces the related restriction, thus enabling you to test those features as if the app were unwrapped.
- If disabling a policy fixes the problem, the issue might be that the app isn’t checking for errors in the associated APIs.
- If an unmodified but re-signed app doesn’t run:
-
Un-jar the contents of the APK using JAR:
jar xvf {some.apk}
-
Delete the META-INF folder:
rm -rf META-INF
-
Re-jar the contents into a new APK using JAR:
jar cvf {/tmp/new.apk} *
-
Sign the new APK using JARSIGNER:
jarsigner -keystore {some.keystore} -storepass {keystorepassword} -keypass {keypassword} {/tmp/new.apk} {keyalias}
-
If the app still doesn’t run, you cannot wrap the app using a different signing certificate than the original APK used.
-
- If a decompiled or recompiled .apk doesn’t run:
-
Decompile and recompile using APKTOOL:
apktool d {some.apk} -o {some.directory}
apktool b {some.directory} -o {new.apk}
-
Sign the APK using JARSIGNER as described above.
-
If the app still doesn’t run, this is a third-party APKTOOL bug.
-
- If app wrapping doesn’t work:
- Try removing the APKTOOL framework and rewrapping.
- Mac/Linux: rm -rf ~/Library/apktool/framework
- Windows: del /q /s C:\Users\{username}\apktool\framework
- Compare which APKTOOL is being used by the wrapper with the one you used to successfully decompile and recompile in the previous step.
- If it is the same APKTOOL version, then there is a bug in Wrapper.
- If it is a different APKTOOL version, then there might be a bug in the APKTOOL integrated into the MDX Toolkit utility.
- Un-jar the contents of ManagedAppUtility.jar.
- Overwrite with contents of APKTOOL.jar that you used to successfully wrapped the app in the previous step.
- Re-jar the contents into a new ManagedAppUtility.jar.
- Wrap the app to confirm the bug in the embedded APKTOOL.
- Try removing the APKTOOL framework and rewrapping.
- Run the wrapped app and capture log information.
-
Use grep to investigate what is happening in the app.
To follow the app’s Activities: grep “MDX-Activity”
To follow MDX locking of the app: grep “MDX-Locked”
To see both logs together: egrep “MDX-Act MDX-Loc” -
If there is an Application Not Responding error, pull the ANR traces using ADB.
-
- If a problem occurs when interacting with multiple apps, such as when using Open in:
- Verify encryption policies and security group settings are the same between the apps.
- Try a different app. It might be a bug in one of the apps being tested.
- Capture logs from all apps involved. Note that Secure Hub can bundle logs and email logs from individual apps. From the My Apps screen, swipe right to the Support screen. Then click the Need Help button at the bottom of the screen.
In addition to the tools mentioned above, the following might also help:
-
AAPT to dump information about the app.
aapt dump badging {some.apk}
-
DUMPSYS command on device.
adb shell dumpsys 2>&1 | tee {dumpsys.out}
-
DEX2JAR to recompile classes into pseudo-Java.
dex2jar {some.apk}
Convert classes from Dual-Dex wrapped apps:
apktool d {some.apk} -o {some.dir}
dex2jar {some.dir}/assets/secondary-1.dex
-
JD-GUI to view pseudo-Java code.
-
BAKSMALI to decompile app classes from Dual-Dex wrapped apps.
-
Decompile the wrapped APK:
apktool d {some.apk} -o {some.dir}
-
Decompile the app’s classes that do not get decompiled from above call:
baksmali {some.dir}/assets/secondary-1.dex -o {some.dir}/smali
-