ACTION_GET_CONTENT / ACTION_OPEN_DOCUMENT to open ASD? (WebViewExtra)

In my WebViewExtra extension, for a file upload, I have an intent that opens the "file picker", currently to the Shared Directories area:

protected Intent getIntent() {
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType(fileType);
      return Intent.createChooser(i, "File Chooser");
  }

Is it possible in any way to open the file picker to the app's ASD to allow the user to select from their own files stored/created there ?

If it is of any help, I have been able to get WebViewExtra to download a file to the ASD:

req.setDestinationInExternalFilesDir(context, "/", nameFile);

Can make full source available if needed.

It is possible to do in Android 12 and below but not in last two versions.

1 Like

Stinks given the app has read/write access to its own files in the ASD...

Guess the only option is to copy all files in the ASD to /Documents ?

It's more work, but you could bundle an activity that lists the files in the ASD and presents them in a list. Then, when people trigger the picker, ask if they want content from this app or shared data and take the appropriate branch.

I am interested....

Actually, you may not even have to write the activity. The ListPickerActivity App Inventor uses for the ListPicker component is technically bundled with every app (it just isn't in the manifest if the ListPicker isn't used). You would activate it in the manifest using the @UsesActivities annotation as you can see at line 57 of ListPicker.java:

You need to register your extension as an ActivityResultListener interface to receive the selection and launch the activity when needed:

Then it's just a matter of listing the files in the ASD and providing that in the Intent. Nested directories might be harder if that's a use case you are considering.

Thank you Evan, will have a look at this.

(noting I am building with RUSH...)

Hmmm, I have reached the limits of my capability :upside_down_face:

Extension Builder - RUSH
IDE - Intellij (using latest runtime.jar from Shreyash)

  1. I put this
<activity android:name="com.google.appinventor.components.runtime.ListPickerActivity"/>

in the AndroidManifest.xml

  1. In the java, I have
import com.google.appinventor.components.runtime.*;

in imports (which will include ListPickerActivity)

I added this above the existing intent:

protected Intent getIntentLPI() {
    Intent lpi = new Intent();
    lpi.setClassName(container.$context(), LIST_ACTIVITY_CLASS);
    YailList items = null;
    lpi.putExtra(LIST_ACTIVITY_ARG_NAME, items.toStringArray());
    return lpi;
  }

LIST_ACTIVITY_CLASS and LIST_ACTIVITY_ARG_NAME are unrecognised symbols, so not being picked up as a part of the ListPickerActivity.

Not sure where to go next?

Section for Uploading without any changes
  public class ChromeClient extends WebChromeClient {
    public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> filePath, WebChromeClient.FileChooserParams fileChooserParams) {

      if (mFilePathCallback != null) {
        mFilePathCallback.onReceiveValue(null);
      }
      mFilePathCallback = filePath;

      click();
      return true;
    }
  }

  public void click() {
    BeforePicking(fileType);
    if (requestCode == 0) {
      requestCode = container.$form().registerForActivityResult(this);
    }
    container.$context().startActivityForResult(getIntent(), requestCode);
  }

  protected Intent getIntent() {
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType(fileType);
      return Intent.createChooser(i, "File Chooser");
  }

  public void resultReturned(int requestCode, int resultCode, Intent data) {
    Uri[] results = null;

    if (resultCode == Activity.RESULT_OK) {
      String dataString = data.getDataString();
      if (dataString != null) {
        results = new Uri[]{Uri.parse(dataString)};
        AfterPicking(getFileName(Uri.parse(dataString)));
      }
    }
    mFilePathCallback.onReceiveValue(results);
    mFilePathCallback = null;
  }

  public String getFileName(Uri uri) {
    String result = null;
    if (uri.getScheme().equals("content")) {
      try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
        if (cursor != null && cursor.moveToFirst()) {
          result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
        }
      }
    }
    if (result == null) {
      result = uri.getPath();
      int cut = result.lastIndexOf('/');
      if (cut != -1) {
        result = result.substring(cut + 1);
      }
    }
    return result;
  }

From where did you pick that code, that gives you the error?

They are fields declared in the class ListPicker

Replace LIST_ACTIVITY_CLASS with string com.google.appinventor.components.runtime.ListPickerActivity

and LIST_ACTIVITY_ARG_NAME with string com.google.appinventor.components.runtime.ListPickerActivity.list


Edit: One more thing to note, you are missing to add tag exported to your activity declaration. (Which may error while installing the app)

Correction:

        <activity android:name="com.google.appinventor.components.runtime.ListPickerActivity"
                  android:exported="true"/>
4 Likes

@Kumaraswamy to the rescue once again ! :slight_smile:

I have applied the changes and all errors gone (not tested yet because...)

Final piece of the puzzle, should I generate the list in the app then pass it to the extension, or do something (more code) in the extension to generate the list?

Would probably need help with the latter...

I guess this would need to be a method so that it could be run at initalisation and after files have updated.

Sure, but what kind of list do you want to generate and where will you use that?

YailList

In the intent to set the listpicker values

protected Intent getIntentLPI () {
    Intent lpi = new Intent();
    lpi.setClassName(container.$context(), "com.google.appinventor.components.runtime.ListPickerActivity");
    YailList items = null;
    lpi.putExtra("com.google.appinventor.components.runtime.ListPickerActivity.list", items.toStringArray());
    return lpi;
  }

Oh alright, will the extension user "simply" input the list? Or do you want to manipulate it in some way before passing as listpicker values?

It should be a list of all files in the ASD.

It would be good to handle sub folders and show the paths to these. e.g.

myfile.pdf
redSquare.jpg
images/square.jpg
images/circle.png
data/texts/data1.txt
texts/data2.csv
...

The extension could then handle the file path for the file upload.

  @SimpleFunction
  public void AstAction(YailList fileNames) {
    String[] items = fileNames.toStringArray();
    for (int i = 0, len = items.length; i < len; i++) {
      String item = items[i];
      // what do you want to do now? append the full ASD Path?
      // you can do:
      // item = YOUR_ASD_PATH + '/' + item;

      // then update the item
      items[i] = item;
    }
    ... some code
    lpi.putExtra("com.google.appinventor.components.runtime.ListPickerActivity.list", items);
    ...
  }

Thank you
Will have a play around with this and report back!

1 Like

OK, good news!

Have it working so that I can switch between selecting files in Shared Directories or in the ASD

I am using the File component to set the file listing in the ASD, and the ASDPath.

The listpicker show up when "ASD" is selected and shows a list of files with absolute paths.

:clap: :clap: :clap: @ewpatton & especially @Kumaraswamy

Now then, nothing happens after I make a selection from the listpicker. No filename is returned to the web page (not being picked up by the input tag) - I tested with a full path as well.

Need to return the file path from the listpicker selection ?

Here is the relevant java code
public class ChromeClient extends WebChromeClient {
    public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> filePath, WebChromeClient.FileChooserParams fileChooserParams) {

      if (mFilePathCallback != null) {
        mFilePathCallback.onReceiveValue(null);
      }
      mFilePathCallback = filePath;

      click();
      return true;
    }
  }

  public void click() {
    BeforePicking(fileType);
    if (requestCode == 0) {
      requestCode = container.$form().registerForActivityResult(this);
    }
    if ( defaultDirectory == false ) {
      container.$context().startActivityForResult(getIntent(), requestCode);
    } else {
      container.$context().startActivityForResult(getIntentLPI(), requestCode);
    }
    }

    protected Intent getIntentLPI () {
    Intent lpi = new Intent();
    lpi.setClassName(container.$context(), "com.google.appinventor.components.runtime.ListPickerActivity");
    lpi.putExtra("com.google.appinventor.components.runtime.ListPickerActivity.list", asdFiles);
    return lpi;
  }


  protected Intent getIntent() {
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType(fileType);
      return Intent.createChooser(i, "File Chooser");
  }

  public void resultReturned(int requestCode, int resultCode, Intent data) {
    Uri[] results = null;

    if (resultCode == Activity.RESULT_OK) {
      String dataString = data.getDataString();
      if (dataString != null) {
        results = new Uri[]{Uri.parse(dataString)};
        AfterPicking(getFileName(Uri.parse(dataString)));
      }
    }
    mFilePathCallback.onReceiveValue(results);
    mFilePathCallback = null;
  }

  public String getFileName(Uri uri) {
    String result = null;
    if (uri.getScheme().equals("content")) {
      try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
        if (cursor != null && cursor.moveToFirst()) {
          result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
        }
      }
    }
    if (result == null) {
      result = uri.getPath();
      int cut = result.lastIndexOf('/');
      if (cut != -1) {
        result = result.substring(cut + 1);
      }
    }
    return result;
  }

I am guessing work needs to be done in the resultReturned / getFileName areas ?

1 Like

Looking at the implementation of resultReturned in the ListPicker class, it looks like you will need to call data.getStringExtra("com.google.appinventor.components.runtime.ListPickerActivity.selection") to extract the selection value returned from the ListPickerActivity. It's not returned in the data URI portion of the Intent.

2 Likes

I tried this, but app crashes on select:

public void resultReturned(int requestCode, int resultCode, Intent data) {
    Uri[] results = null;
    String dataString;

    if (resultCode == Activity.RESULT_OK) {

      if (uploadDirectory) {
        dataString = data.getStringExtra("com.google.appinventor.components.runtime.ListPickerActivity.selection");
      } else {
        dataString = data.getDataString();
      }

      if (dataString != null) {
        results = new Uri[]{Uri.parse(dataString)};
        AfterPicking(getFileName(Uri.parse(dataString)));
      }
    }
    mFilePathCallback.onReceiveValue(results);
    mFilePathCallback = null;
  }

uploadDirectory is a boolean, set to true for using the ASD