When I created ActiveVault, one of the thoughts was to also try to protect Java code. However, whatever way I looked at it, it wasn’t possible. You can encrypt code but as soon as you decrypt and load the Java, it becomes visible in two ways:
The first is that, since ART and Android 4.4, it’s not possible to dynamically load (decrypted) Java code without it appearing in storage. Previously, with Dalvik, it was possible to dynamically load extra code to memory. For devices with the ART runtime, an OAT file containing the DEX file has to exist in the file store, in the dalvik-cache directory. This is world readable even on un-rooted, unmodified devices. The technical reason why the file has to be in storage is so that it can be paged out by the OS. There’s DexExtractor available to extract such files on the emulator.
The second, more involved way, that involves tampering with the OS, is via hooked functions. This is where attackers can intercept function calls to do extra things such as read files/data. Utilities such as DexHook and DexHunter (research paper pdf) can intercept the code as it is dynamically loaded.
Yes, there are some code protection utilities or packers that claim to encrypt code or classes. These do what they say, they encrypt code, but they are of not much use if the code is easily readable later on when processed by the OS. There’s no way to fully protect Java code. It can only be obfuscated.