Page 1 of 1

Android Extract Assets / Add to File List

Posted: Thu May 10, 2018 5:21 am
by LunaRebirth
I figured this might be useful for others who don't know about it.

Basically,
there is no way to read asset files using fopen() in C++ on Android.
Similarly, Irrlicht cannot find asset images on Android without first knowing which directories exist in the assets (Irrlicht doesn't do this automatically).

Here is some code I put together to make Irrlicht automatically extract files and find directories in the asset folder.
This allows you to not worry about doing it yourself, and in the case that you need to read/write local files often (like me).

First, you'll need to add this to your MainActivity:

Code: Select all

//...
public class MainActivity extends NativeActivity {
    //...
    // Method:    listAssets
    // Params:
    //     path:       The path to recursively search for files and/or folders
    //     files:       If true, also returns file names (such as .txt, .png, etc)
    //     folders:   If true, also returns folder names (directories)
    //        (for the sake of this snippet, we only want folders to be true)
    // Returns:
    //     ArrayList<String> of all files and/or folders recursively searched starting at the directory (path).
    public ArrayList<String> listAssets(String path, boolean files, boolean folders) {
        String [] list;
        ArrayList<String> found = new ArrayList<String>();
        try {
            list = getAssets().list(path);
            if(list.length > 0){
                for(String file : list){
                    System.out.println("File path = "+file);
 
                    if(file.indexOf(".") < 0) { // Folder
                        if (path.length() > 0)
                            file = path + "/" + file;
                        if (folders)
                            found.add(file);
                        ArrayList<String> newFound = listAssets(file, files, folders); // File
                        for (String newFile : newFound)
                        {
                            found.add(newFile);
                        }
                    }else{
                        if (path.length() > 0)
                            file = path + "/" + file;
                        if (files)
                            found.add(file);
                    }
                }
 
            }
        }catch(IOException e){
            e.printStackTrace();
        }
        return found;
    }
    // ...
}
Now, in C++ we can get our list of assets from Java and add them to the archive, allowing Irrlicht to find images and models.
We also extract them by reading and then writing the files (this may not be necessary if you use Irrlicht's IReadFile, but in my case I am wanting to use fopen and fwrite).

Code: Select all

void androidCreateFilePath()
{
    JNIEnv* jniEnv = 0;
    // The app variable is from Irrlicht's Android main method (void android_main(android_app* app))
    app->activity->vm->AttachCurrentThread(&jniEnv, NULL);
 
    //////////
 
    // Here is where we call the MainActivity "listAssets" method
    jobject objectActivity = app->activity->clazz;
    jclass classActivity = jniEnv->GetObjectClass(objectActivity);
    //      ZZ means boolean, boolean
    jmethodID methodGetOpenFileName = jniEnv->GetMethodID(classActivity, "listAssets", "(Ljava/lang/String;ZZ)Ljava/util/ArrayList;");
    jobject arrayList = jniEnv->CallObjectMethod(objectActivity, methodGetOpenFileName, jniEnv->NewStringUTF(""), false, true);
 
    /////
 
    // Taken from online, this converts the above arrayList jobject to an std::vector<std::string>
    static jclass java_util_ArrayList       = static_cast<jclass>(jniEnv->NewGlobalRef(jniEnv->FindClass("java/util/ArrayList")));
    static jmethodID java_util_ArrayList_   = jniEnv->GetMethodID(java_util_ArrayList, "<init>", "(I)V");
    jmethodID java_util_ArrayList_size      = jniEnv->GetMethodID (java_util_ArrayList, "size", "()I");
    jmethodID java_util_ArrayList_get       = jniEnv->GetMethodID(java_util_ArrayList, "get", "(I)Ljava/lang/Object;");
    jmethodID java_util_ArrayList_add       = jniEnv->GetMethodID(java_util_ArrayList, "add", "(Ljava/lang/Object;)Z");
 
    jint len = jniEnv->CallIntMethod(arrayList, java_util_ArrayList_size);
    std::vector<std::string> result;
    result.reserve(len);
    for (jint i=0; i<len; i++) {
      jstring element = static_cast<jstring>(jniEnv->CallObjectMethod(arrayList, java_util_ArrayList_get, i));
      const char* pchars = jniEnv->GetStringUTFChars(element, nullptr);
      result.push_back(pchars);
      jniEnv->ReleaseStringUTFChars(element, pchars);
      jniEnv->DeleteLocalRef(element);
    }
 
    //////////
 
    // Game::device is our IrrlichtDevice
    for ( u32 i=0; i < Game::device->getFileSystem()->getFileArchiveCount(); ++i )
    {
        IFileArchive* archive = Game::device->getFileSystem()->getFileArchive(i);
        if ( archive->getType() == EFAT_ANDROID_ASSET )
        {
            // For all of our asset's folders found from the MainActivity "listAssets" method, add it to our file list
            for (std::string folder : result)
            {
                archive->addDirectoryToFileList((char*)(folder + "/").c_str());
            }
 
            // Get every file from the archive, and extract it if it isn't already.
            // Again, probably not necessary unless you are planning to update files often.
            IFileList* files = (IFileList*)archive->getFileList();
            for (int x = 0; x < files->getFileCount(); x++)
            {
                std::string fName = app->activity->internalDataPath + files->getFullFileName(x).c_str();
                // If the file is not a directory and it doesn't already exist (you can use stat() or directly try fread() to check if the file exists)
                if (!files->isDirectory(x) && !Operations::fileExists(fName))
                {
                    IReadFile* f = archive->createAndOpenFile(files->getFullFileName(x));
                    int len = f->getSize();
                    char* buf = new char[len+1];
                    f->read(buf, len);
                    f->drop();
                    buf[len] = '\0';
 
                    // This method is pretty simple. Simply tokenize the fName string by '/' and '\\' and use mkdir(split, 0770) for Android.
                    //   The idea there is to create any necessary folders where we should store the extracted file.
                    Operations::createDirectories(fName);
                    FILE* fi = fopen(fName.c_str(), "wb");
                    fwrite(buf, 1, len, fi);
                    fclose(fi);
 
                    delete [] buf;
                }
            }
            break;
        }
    }
}
That's it. Hopefully this will help anyone who needs it.
I was previously hard-coding directories to the archive, which was a huge hassle when I keep moving stuff around.