Asset path when using Companion vs. after building the app (Extension development)

Hi everyone,

I'm currently building an extension for MIT App Inventor, and I need to load a file from the assets folder (such as an json file or a custom font). I'm a bit confused because the file path seems to be different depending on whether the app is running in the Companion or if it has been built and installed.

So my questions are:

  1. What is the correct path to access assets when the app is running in the Companion?
  2. What is the correct path when the app is built (APK/AAB) and installed on a device?

Since this is part of an extension, I want to make sure the file can be accessed properly in both cases. Any guidance would be really helpful. Thanks in advance!

Using the Companion, The assets are served via a virtual file system usually accessed using the /sdcard/AppInventor/assets/ path (based on Android version or setup).

once the app is built (APK or AAB) & installed, The assets are bundled inside the APK & you typically need to use the getAssets().open("filename") approach via the Android AssetManager.

1 Like
1 Like
  • APK (Installed App): When you build and install your app (the APK), all your uploaded asset files are bundled inside the APK package itself. Android has a standard way to access these internal assets.
  • Companion App: When you are live testing using the Companion app, your assets are not inside the Companion app's own APK. Instead, the development environment (the website builder) sends your assets over the network to the Companion, which usually saves them to a specific folder on your phone's storage (like the SD card or internal storage). The Companion then needs to load the file from that external location.

How the Code Figures Out Which Mode It's In:

The code uses a simple check:

  1. isCompanion() Function: This function checks the app's unique identifier, called the "package name".
  • If the package name belongs to one of the known Companion apps (like edu.mit.appinventor.aicompanion... or io.kodular.companion...), it knows it's running in Companion mode .

  • Otherwise, it assumes it's running as a standalone APK .

private boolean isCompanion() {
  try {
    // Get the app's package name
    String packageName = this.form.$context().getPackageName();

    // Check if the name matches known Companion apps
    return packageName.contains("edu.mit.appinventor.aicompanion") ||
           packageName.contains("io.kodular.companion") ||
           packageName.contains("com.niotron.companion") ||
           /* other potential companion names */;
  } catch (Exception e) {
    return false; // If anything goes wrong, assume it's an APK
  }
}

How Files are Loaded in Each Mode (Simplified for Any File):

Let's imagine you want to load a file named my_custom_data.json from your assets.

  1. If Running as an APK (isCompanion() is false):
  • Where's the file? Inside the APK package.

  • How to load? Use Android's built-in AssetManager. This is the standard, reliable way.

// Simplified APK Loading
if (!isCompanion()) {
    Log.i("MyApp", "Running as APK. Loading 'my_custom_data.json' from assets.");
    try {
        // Get the standard Asset Manager
        android.content.res.AssetManager assetManager = context.getAssets();
        // Open the file directly from the bundled assets
        java.io.InputStream inputStream = assetManager.open("my_custom_data.json");
        // Now you can read the file from 'inputStream'
        Log.i("MyApp", "Successfully opened file from APK assets.");
        // ... process the inputStream ...
        inputStream.close();
    } catch (java.io.IOException e) {
        Log.e("MyApp", "Error loading file from APK assets", e);
    }
}
  1. If Running via Companion (isCompanion() is true):
  • Where's the file? Somewhere on the phone's external storage (the exact path can vary depending on the builder platform and Android version). The code tries to guess this path.

  • How to load? Treat it like a regular file on the phone's storage using standard Java file operations. This is less reliable because file paths can change, and storage permissions might be needed.

// Simplified Companion Loading
else { // isCompanion() is true
    Log.i("MyApp", "Running in Companion. Loading 'my_custom_data.json' from external storage.");
    String fileName = "my_custom_data.json";
    String companionPath = ""; // This needs to be calculated carefully!

    // *** VERY SIMPLIFIED Path Guessing (like in the font code) ***
    // The real code is more complex, checking Android version etc.
    // This is just an example path structure
    String platform = "AppInventor"; // or "Kodular", "Niotron" etc. based on package name
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { // Android 10+
         // Might be in app-specific external dir (preferred if possible)
         File externalAssetsDir = context.getExternalFilesDir("assets");
         if (externalAssetsDir != null) {
             companionPath = externalAssetsDir.getAbsolutePath() + "/" + fileName;
         } else {
            // Fallback to older-style path structure (less reliable)
             companionPath = "/storage/emulated/0/Android/data/" + context.getPackageName() + "/files/assets/" + fileName;
         }
    } else { // Older Android
        companionPath = "/storage/emulated/0/" + platform + "/assets/" + fileName;
    }
    Log.d("MyApp", "Trying Companion path: " + companionPath);
    // ***************************************************************

    try {
        java.io.File file = new java.io.File(companionPath);
        if (file.exists() && file.canRead()) {
            // Open the file using standard file input
            java.io.InputStream inputStream = new java.io.FileInputStream(file);
            // Now you can read the file from 'inputStream'
            Log.i("MyApp", "Successfully opened file from Companion path.");
            // ... process the inputStream ...
            inputStream.close();
        } else {
            Log.e("MyApp", "File not found or not readable at Companion path: " + companionPath);
            // *** Companion Fallback Attempt (like in the font code) ***
            // Sometimes, even in Companion, trying the AssetManager might work
            // depending on the specific Companion version/implementation.
             Log.w("MyApp", "Companion file path failed, trying AssetManager fallback...");
             try {
                 android.content.res.AssetManager assetManager = context.getAssets();
                 java.io.InputStream fallbackStream = assetManager.open(fileName);
                 Log.i("MyApp", "Companion AssetManager fallback SUCCESSFUL.");
                 // ... process fallbackStream ...
                 fallbackStream.close();
             } catch (java.io.IOException assetEx) {
                 Log.e("MyApp", "Companion AssetManager fallback FAILED.", assetEx);
             }
             // ***********************************************************
        }
    } catch (java.io.FileNotFoundException e) {
         Log.e("MyApp", "File not found exception at Companion path: " + companionPath, e);
    } catch (java.io.IOException e) {
        Log.e("MyApp", "Error reading file from Companion path", e);
    }
}

In Simple Terms:

  • APK: File is inside the app package -> Use AssetManager (easy, reliable).

  • Companion: File is outside the app, on phone storage -> Use File access with a specific path (harder, less reliable, path varies, needs permissions). The code tries to guess the path and might even try the AssetManager as a backup plan if the file path fails.

3 Likes

what about using

private boolean isRunningInCompanion = Form.getActivityForm() instanceof ReplForm; // check if it it's being run in the Companion
1 Like

Some basics on Android storage system

1. Internal Storage
The Internal Storage can only be accessed with a rooted device.

1.1 The app package is saved in

/data/data/<packageName>/

In order to be able to debug your app, AI2 saves the assets for → Companion on devices with

  • Android ≥ 10 (API ≥ 29):
/storage/emulated/0/Android/data/edu.mit.appinventor.aicompanion3/files/assets/
  • Android < 10 :
/storage/emulated/0/Android/data/edu.mit.appinventor.aicompanion3/files/AppInventor/assets/
1 Like

1 Like

Are you talking about assets provided by the user or assets provided by your extension?

If the former, you want to call form.openAsset(String) with the asset name, e.g., "kitty.png", which will open the asset and return an InputStream. You are responsible for closing the stream when you are done with it.

If the latter, you want to call form.openAssetForExtension(Component, String) with an instance of your extension and the name of the asset you've bundled (make sure to include it in the @UsesAssets annotation too). This will return an InputStream for the desired asset.

Both functions are companion aware so you don't need to have alternate code paths checking form.isRepl().

4 Likes

Hi everyone,

I really appreciate all the help and suggestions you provided. Thanks to your guidance, I finally got my code working!

Here’s the working example for reference:

package com.otniel.testpath;

import com.google.appinventor.components.annotations.*;
import com.google.appinventor.components.common.*;
import com.google.appinventor.components.runtime.*;
import java.io.*;
import java.nio.charset.StandardCharsets;

@DesignerComponent(
    version = 2,
    description = "Extension untuk membaca file yang diupload pengguna (via panel Media)",
    nonVisible = true,
    iconName = "icon.png"
)
public class Testpath extends AndroidNonvisibleComponent {
    
    private static final int BUFFER_SIZE = 4096; 
    
    public Testpath(ComponentContainer container) {
        super(container.$form());
    }

    @SimpleFunction(description = "Baca file teks yang diupload user (JSON, TXT, dll)")
    public String ReadTextFile(String fileName) {
        try (InputStream inputStream = form.openAsset(fileName);
             BufferedReader reader = new BufferedReader(
                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            
            StringBuilder content = new StringBuilder();
            char[] buffer = new char[BUFFER_SIZE];
            int charsRead;
            
            while ((charsRead = reader.read(buffer)) != -1) {
                content.append(buffer, 0, charsRead);
            }
            
            return content.toString();
            
        } catch (IOException e) {
            return "Error: " + e.getMessage();
        }
    }

    @SimpleFunction(description = "Baca file binary (TTF, PNG, dll) dan return sebagai list-byte")
    public Object ReadBinaryFile(String fileName) {
        try (InputStream inputStream = form.openAsset(fileName);
             ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
            
            byte[] data = new byte[BUFFER_SIZE];
            int bytesRead;
            
            while ((bytesRead = inputStream.read(data)) != -1) {
                buffer.write(data, 0, bytesRead);
            }
            
            return buffer.toByteArray();
            
        } catch (IOException e) {
            return "Error: " + e.getMessage();
        }
    }

    @SimpleFunction(description = "Cek apakah file ada di Media")
    public boolean FileExists(String fileName) {
        try (InputStream test = form.openAsset(fileName)) {
            return true;
        } catch (IOException e) {
            return false;
        }
    }
}

for test
aia :
test_path.aia (33.2 KB)

com.otniel.testpath.aix (6.0 KB)

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.