cancel
Showing results for 
Search instead for 
Did you mean: 

Scoped Storage and VR

SlashAndBurn
Expert Protege

Ok, So..... I need to use Shared Storage to access folders that the users wants.  I know it works, since flat apps like Amaze file manager can do the entire flow, but when I try from my VR app written in Unity, it fails to receive the request.

What I have done

  1. Made a Jar plugin
  2. Added in a Activity to receive the requests
  3. From c# call into Java
  4. Start a new activity to receive the result, this makes VR go blank
  5. From java generate the OPEN_TREE... Intent and pass in a URI
  6. The Activity never receives the result after the popup is presented
1 ACCEPTED SOLUTION

Accepted Solutions

SlashAndBurn
Expert Protege

I got it to work, but it was a mess, the OS has a bug, it has a bug, and I know this because I literally have to crash the VR stack in order for the app to receive the response from Scoped Storage.  But it does work, launch a NON-VR Activity, have it crash to the Home by pressing the Oculus button, and then come back into the app, response received.

View solution in original post

9 REPLIES 9

SlashAndBurn
Expert Protege

I do have some source code, and it feels like it should work, but something ain't quite right.

 

package com.mgatelabs.mvrs.fixes;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.util.Log;

public class DeviceAccess {

    public static void GrantAllFilesAccess(Activity activity) {
        Uri uri = Uri.parse("package:com.mgatelabs.mobilevrstationthree");
        activity.startActivity(
                new Intent(
                        "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION",
                        uri
                )
        );
    }

    public static Uri GetContentFromPath(String path) throws Exception {

        String absPath = Environment.getExternalStorageDirectory().getAbsolutePath();

        Log.d("AskForAccess", "absPath: " + absPath);

        int offset = path.indexOf(absPath);
        String suffix = path.substring(offset + absPath.length());
        Log.d("suffix", "suffix: " + absPath);

        String documentId = "primary:" + suffix.substring(1);

        Log.d("AskForAccess", "documentId: " + documentId);

        return DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", documentId);
    }

    public static void AskForAccess(final Activity activity, String path) throws NoSuchMethodException {
        try {
            int flags = 0; //0x00000040 | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;

            //flags |= 0x00000040;
            flags |= Intent.FLAG_GRANT_READ_URI_PERMISSION;
            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;

            /*
            Intent subIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
            subIntent.putExtra("android.provider.extra.INITIAL_URI", GetContentFromPath(path));                  // give changed uri to Intent
            subIntent.addFlags(flags);
            Log.i("AskForAccess", "starting sub intent request");
            activity.startActivityForResult(subIntent, 1024);
            */

            DeviceActivity.selectedPath = GetContentFromPath(path);
            DeviceActivity.flags = flags;

            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    Intent intent = new Intent();
                    intent.setAction(Intent.ACTION_SEND);
                    intent.setClass(activity, DeviceActivity.class);
                    activity.startActivity(intent);
                }
            });

        } catch (Exception e) {
            Log.e("AskForAccess", "ErrBdy", e);
        }
    }
}

 

And the activity to receive the requests

 

 

package com.mgatelabs.mvrs.fixes;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

public class DeviceActivity extends Activity {

    public static Uri selectedPath;
    public static int flags;

    public static ContentResolver contentResolver;

    public static final String LOGTAG = "ResolverClass";

    void myFinish(int result)
    {
        //if (shareImageCallback!=null)
        //    shareImageCallback.onShareComplete(result);
        //shareImageCallback = null;
        finish();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.i(LOGTAG, "onResume");

        try
        {
            Log.i(LOGTAG, "making sub intent");
            Intent subIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
            subIntent.putExtra("android.provider.extra.INITIAL_URI", selectedPath);                  // give changed uri to Intent
            subIntent.addFlags(flags);
            Log.i(LOGTAG, "starting sub intent request");
            startActivityForResult(subIntent, 1);

        }
        catch (Exception e)
        {
            Log.i(LOGTAG,"error: " + e.getLocalizedMessage());
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(LOGTAG, "onCreateBundle");


    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(LOGTAG,"onActivityResult: " + requestCode + ", " + resultCode + ", " + data);
        if (requestCode == 1 && data != null) {
            try {
                this.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            } catch (Exception e) {
                Log.e(LOGTAG, "onActivityResult", e);
            }
        }
        myFinish(resultCode);
    }

}

After I call AskForAccess

  1. The screen goes blank
  2. It presents the chooser
  3. I can press the use this folder thing
  4. It will ask me, are you sure for this app
  5. And then it just hangs, no response to the activity

 

SlashAndBurn
Expert Protege

I've been trying all week to come up with a solution for Scoped Storage for a VR app and I cannot seem to make it work.

I've tried to use the old way of calling an intent, and modified unity's activity to log, and it does not responsd.

activity.runOnUiThread(new Runnable() {
                @Override
                public void run () {
                    Intent subIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
                    subIntent.putExtra("android.provider.extra.INITIAL_URI", DeviceActivity.selectedPath);                  // give changed uri to Intent
                    subIntent.addFlags(DeviceActivity.flags);
                    Log.i("AskForAccess", "starting sub intent request");
                    activity.startActivityForResult(subIntent, 22);
                }
            });

 I've tried to use a Android X activity, which causes the screen to go blank and it doesn't work also.

private final ActivityResultLauncher<Uri> mDirRequest = registerForActivityResult(
            new ActivityResultContracts.OpenDocumentTree() {
                @NonNull
                @Override
                public Intent createIntent (@NonNull Context context, @Nullable Uri input) {
                    super.createIntent(context, input);
                    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && input != null) {
                        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input);
                    }
                    intent.setExtrasClassLoader(origClassLoader);
                    intent.setFlags(flags);
                    Log.i(LOGTAG, "createIntent logic");
                    return intent;
                }


            },
            uri -> {
                Log.i(LOGTAG, "onActivityResult: " + uri);
                if (uri != null) {
                    // call this to persist permission across device reboots
                    getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                    // do your stuff
                } else {
                    // request denied by user
                }
                myFinish(0);
            }
    );

mDirRequest.launch(selectedPath);

I tried to use a different method, open file and I don't get a response or see logs about it.

private final ActivityResultLauncher<String[]> mDirRequest2 = registerForActivityResult(
            new ActivityResultContracts.OpenDocument(), uri -> {
                Log.i(LOGTAG, "onActivityResult: " + uri);
            }
    );

mDirRequest2.launch(new String[]{"video/mp4", "txt/plain"});

I did try to use the permission version and it did return a response quickly without presenting a dialog.

private final ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
Log.i(LOGTAG, "requestPermissionLauncher Result: " + isGranted);
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
});
requestPermissionLauncher.launch("android.permission.WRITE_EXTERNAL_STORAGE");

 

I have the feeling that Scoped Storage at the moment isn't working right in V51 for VR apps to interact with it.  Flat apps, like Amaze File Explorer can make the request, get a response and persist the answer.

SlashAndBurn
Expert Protege

I was testing more and found a way to get the app to receive the Scoped Storage response, but it's messy.  I have to make the request from the main activity, and sometimes it will accept, but it won't respond to the app.  Then I have to switch to a broken Activity which force the screen to be blank.  Then press the oculus button until it finally give up and returns to the home screen.  Then I can re-launch the app, which is already open, and it will receive the response.  So there is defiantly a bug in the Meta implementation on Android 12 Scoped Storage.

SlashAndBurn
Expert Protege

I got it to work, but it was a mess, the OS has a bug, it has a bug, and I know this because I literally have to crash the VR stack in order for the app to receive the response from Scoped Storage.  But it does work, launch a NON-VR Activity, have it crash to the Home by pressing the Oculus button, and then come back into the app, response received.

chronicbite
Explorer

Hey! Adding to this thread to say I've observed the same issue. I am building a Unity app with a plugin that performs native Android calls. The plugin is able to evoke the file picker dialog but when I select a file the app never receives the callback I found that locking and unlocking the device has a similar effect to your activity crash trick... but I can't demand users do that every time they pick a file...
 
Can you provide more info on how you fixed the issue on your side? Also, any chance Meta are aware of this bug?

You have to modify the Unity Player View object to receive the response, the "old way", not the fancy handler way.  Also you have to crash the VR stack, but only crash it enough where it responds to your running app when you return.  Also the gradle build can overwrite any changes you make to the java classes in some instances, like a clean build, so you have to take that into account.

But the VR implementation is Bugged and its not user friendly.  If they allowed Quest apps to run in an optional "2D" mode as an option, then maybe in that mode it would work great, since 2D apps don't have this struggle.

I haven't seen any Meta people comment, so I think this is under the radar.  I saw they fixed another android permission bug a while back, but nothing about making scoped storage any better.  I don't think they really care about it, for most of the apps, they don't need it, its just special apps.

Mike_Icosa
Explorer

Hi, thanks for this! We're encountering the same issue. Would you be able to share your full modified android source code for this if possible please, and the steps to call it from Unity?

 

Thankfully Meta are aware of the issue here, but we've had no suggestions or plan for how they're going to help out. There's a whole group of creative app developers who are desperate for a solution.

Mike_Icosa
Explorer

So we have an update:

Meta are fully aware of this bug, but they don't expect it to be scheduled for a fix for months. Their recommended solution is now to request the `MANAGE_EXTERNAL_STORAGE` permission. This is a wide reaching permission that will allow you to act in the same way as you were pre-SDK29, but still pops up a nasty 2D context menu to accept the permission. This is considered by Meta to be intended. This permission is usually intended for file managers and anti-virus apps, and would prevent you from uploading to stricter stores like the Google Play store if you included it in a regular android app.

Here's the code, plus a way to bypass this if you're running on Quest 1:

 

        m_FolderPermissionOverride = false;
...

#if UNITY_ANDROID
        private bool UserHasManageExternalStoragePermission()
        {
            bool isExternalStorageManager = false;
            try
            {
                AndroidJavaClass environmentClass = new AndroidJavaClass("android.os.Environment");
                isExternalStorageManager = environmentClass.CallStatic<bool>("isExternalStorageManager");
            }
            catch (AndroidJavaException e)
            {
                Debug.LogError("Java Exception caught and ignored: " + e.Message);
                Debug.LogError("Assuming this means this device doesn't support isExternalStorageManager.");
            }
            return m_FolderPermissionOverride || isExternalStorageManager;
        }

        private void AskForManageStoragePermission()
        {
            try
            {
                using var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
                using AndroidJavaObject currentActivityObject = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
                string packageName = currentActivityObject.Call<string>("getPackageName");
                using var uriClass = new AndroidJavaClass("android.net.Uri");
                using AndroidJavaObject uriObject = uriClass.CallStatic<AndroidJavaObject>("fromParts", "package", packageName, null);
                using var intentObject = new AndroidJavaObject("android.content.Intent", "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION", uriObject);
                intentObject.Call<AndroidJavaObject>("addCategory", "android.intent.category.DEFAULT");
                currentActivityObject.Call("startActivity", intentObject);
            }
            catch (AndroidJavaException e)
            {
                m_FolderPermissionOverride = true;
                Debug.LogError("Java Exception caught and ignored: " + e.Message);
                Debug.LogError("Assuming this means we don't need android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION (e.g., Android SDK < 30)");
            }
        }
#endif

 

 

Apps already on the main store were made aware that this is the official workaround, but it was never communicated either here on the forums, or through programs like App Lab or Start. We had been unable to upload a new version of Open Brush for months despite making a lot of noise about this through all support channels we could.

I actually have that in my app right now for download file access, and I did figure out how to make scoped storage work, but it involves a bit of madness, crashing the VR stack.

https://www.youtube.com/watch?v=DJ7a4m1Ak9U