1

Follow-up to my previous question: How to run a console program which is packed in an Android APK package?

In my app written in C++Builder 11.2, I might have found a way to run a console program which is packed in the APK. The console program, written in NDK aarch64, is named test_run:

extern "C" JNIEXPORT jstring JNICALL
ExecuteCommand(JNIEnv *env,  jstring cmd) {
    const char *cmd_str = env->GetStringUTFChars(cmd, NULL);

    char* alist[] = {0};
    char mode[] = "0754"; // make it executable
    int i;
    i = strtol(mode, 0, 8);
    int errno;
    if (errno = chmod (cmd_str,i) < 0)
    {
        sprintf(buffer, "%s: error in chmod( %s) - %d (%s)\n",
            cmd_str, mode, errno, strerror(errno));
        ShowMessage(UnicodeString("chmod failed: ")+UnicodeString(buffer));
    }
    execvp(cmd_str, alist); // or FILE* fp = popen(...)

    std::string result = "some result return";
    return env->NewStringUTF(result.c_str());
}

In Button1Click(), I'm calling:

// copy "test_run" executable file from APK installation folder to Documents or Download folder
// then:
::ExecuteCommand(JAVA_ENV, ConvertToJstring(DocumentsFolder + "/test_run") );

Now, in ExecuteCommand() function, calling chmod fails:

Operate permission not allowed

How do I call a console program from my APK package using JNI? Or, maybe there is another way to do this?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
DarkSpy
  • 91
  • 5

2 Answers2

0

I guess you need LegacyPackaging. Which zip the .so in .apk and unzip it to lib/xxx so you can execute it directly. You can set it in module build.gradle file, like this:

packagingOptions {
    jniLibs {
        useLegacyPackaging true
    }
}

Android used to extract the .so to file system from .apk.You can enable this feature by set android:extractNativeLibs="true" to your application tag in AndroidManifest.xml(if you do not have build.gradle file).You can use the .so like a program. It is a regular way. Here is the sample:

val cmd = arrayListOf(
                File(context.applicationInfo.nativeLibraryDir, "xxx.so").absolutePath
        )
ProcessBuilder(cmd).start()

welcomeworld
  • 132
  • 8
  • i am sure that RadStudio has no "build.gradle" included in project workspace, by the way, has Android makes some "special" folder that allow APP to running "third part" program like you said: lib/xxx folder. and .so is dynamic lib, for directly running .so file(actually executable file renamed) is the "tricky thing" or "regular way" ? – DarkSpy Apr 21 '23 at 10:21
  • @DarkSpy Android used to extract the .so to file system from .apk.You can enable this feature by set android:extractNativeLibs="true" to your application tag in AndroidManifest.xml.You can use the .so like a program. It is a regular way. Here is the sample: ` val cmd = arrayListOf( File(context.applicationInfo.nativeLibraryDir, "xxx.so").absolutePath ) ProcessBuilder(cmd).start() ` – welcomeworld Apr 21 '23 at 14:18
  • thanks, I'll try to find the way based your content for implementing to RadStudio. anyway, the .so file is need to "from aarch64 exe file rename to .so" and pack into APK, like "assets/internal" folder, am I right ? – DarkSpy Apr 22 '23 at 00:48
  • @DarkSpy Yes,you need to know how to pack the `.so` into `.apk` by RadStudio. We put it in path `module_root/libs/abi(arm64-v8a/armeabi-v7a/..)/` normaly. The `.so` file will allways be packed into `lib/abi/` when build apk. – welcomeworld Apr 22 '23 at 07:57
  • yep, @welcomeworld, at the deloyment function in RadStudio i put the so into lib/armaebi-v7a but not extract it, i thought it was not "native" lib but "renamed" fake .so file. and AndroidManifest.xml can not add options manually, its like IDE re-write depends on the project settings I think. but anyway, still trying – DarkSpy Apr 22 '23 at 08:16
  • try AndroidManifest.xml modified, but still no .so file extracted, trying again – DarkSpy Apr 22 '23 at 11:21
  • @DarkSpy Please check if the final output apk has packed the `.so` file correctly and the AndroidManifest.xml setup correctly. apk file is a zip file so you can extract it like a normal zip file.After install apk success,you can not view `.so` file in file explorer directly.You can check it by code ,like this Java code example: ``` File nativeDir = new File(getApplicationInfo().nativeLibraryDir); Log.e("SoTest", "nativeLibDir:"+nativeDir.getAbsolutePath()); for (String soFile : nativeDir.list()) { Log.e("SoTest", soFile); } ``` – welcomeworld Apr 22 '23 at 13:19
  • still trying, but still not extracted. has any differences between "arm-v7a" executable or "aarch64" executable which i packed ? UI program is 32bit, will aarch64 executable can not be runs ? anyway, i still have some confusing about the folder, is /data/usr/my_project/lib/arm like default executable running folder ? – DarkSpy Apr 23 '23 at 13:37
  • @DarkSpy Can you provide me with your APK? If you pack `.so` into the `.apk` correctly,it will be extracted to `/data/app/xxxx/xxx/ ` with `x` permission. Even though it is rename from a normal txt to `.so` . – welcomeworld Apr 26 '23 at 16:06
  • as I searching the internet these days i found that its not able to running a console program like linux or windows did, the simular way is like you said, make the console program to .so file, but in this case, it's lost some meaning from a individual program become a .so dynamic library in some way. – DarkSpy May 07 '23 at 10:28
0

This line is wrong, it does not do what you think it does:

if (errno = chmod (cmd_str,i) < 0)

Due to operator precedence, it is evaluated as-if you had written it like this:

if (errno = (chmod (cmd_str,i) < 0))

ie, the return value of chmod() is first compared to < 0, and then that boolean result is assigned to errno. So, if chmod() succeeds then you end up setting errno to 0, but if chmod() fails then you end up setting errno to 1, which is EPERM, hence the Operate permission not allowed error message.

You need to write that expression like this instead:

if ((errno = chmod (cmd_str,i)) < 0)

Or, like this:

errno = chmod (cmd_str,i);
if (errno < 0)

ie, the return value of chmod() is first assigned to errno, and then that value is compared to < 0.

That being said, you really should not be naming your local variable as errno, as there is already a standard errno variable which chmod() sets on failure. Name your local variable something more unique instead, and then use the standard errno when formatting your error message. In which case, you can just get rid of your local variable altogether, eg:

if (chmod (cmd_str,i) < 0)

Also, after calling env->GetStringUTFChars(), you need to call env->ReleaseStringUTFChars() or else you will leak the memory.

And, you can replace ::sprintf() with either UnicodeString::sprintf() or UnicodeString::Format() to simplify the formatting of your error message.

Try this:

extern "C" JNIEXPORT jstring JNICALL
ExecuteCommand(JNIEnv *env,  jstring cmd) {
    const char *cmd_str = env->GetStringUTFChars(cmd, NULL);

    const char mode[] = "0754"; // make it executable
    int i = strtol(mode, NULL, 8);
    if (chmod (cmd_str, i) < 0)
    {
        UnicodeString msg = UnicodeString().sprintf(L"chmod failed: %hs: error in chmod(%hs) - %d (%hs)\n",
            cmd_str, mode, errno, strerror(errno));
        /* alternatively:
        UnicodeString msg = UnicodeString::Format(_D("chmod failed: %s: error in chmod(%s) - %d (%s)\n"),
            ARRAYOFCONST(( cmd_str, mode, errno, strerror(errno) )) );
        */
        ShowMessage(msg);
    }

    char* alist[] = {NULL};
    execvp(cmd_str, alist); // or FILE* fp = popen(...)

    env->ReleaseStringUTFChars(cmd, cmd_str);

    std::string result = "some result return";
    return env->NewStringUTF(result.c_str());
}

On a side note, be aware that env->GetStringUTFChars() and env->NewStringUTF() both operate on Java's Modified UTF-8, not standard UTF-8. This is not an issue if your strings contain ASCII characters only, but you might run into problems if they contain non-ASCII characters. Android runs on top of Linux, and Linux filesystem paths use standard UTF-8.

So, you may need to convert between Modified UTF-8 and standard UTF-8 when making external calls. Otherwise, consider operating on Java's native (and standard) UTF-16 encoding instead, using env->(Get|Release)StringChars() and env->NewString(), and converting between UTF-16 and standard UTF-8 when needed, using another library, or your own manual code.

Or, you can convert a jstring to standard UTF-8 using Java's own String.getBytes(Charset)1 or String.getBytes(CharsetName) method. And create a new jstring from standard UTF-8 using Java's native String(byte[], Charset)1 or String(byte[], CharsetName) constructors.

1 Java's StandardCharsets.UTF_8 is a Charset object for standard UTF-8.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770