This article is assuming you got an AndroidJavaObject already from somewhere. That's your Java object that Unity had JNI-ed for you.

You can specify <AndroidJavaObject> in various places

For example If you made a static method returning int waiting at Java, then you have AndroidJavaClass, you might know you can use ajc.CallStatic<int>. But if you are returning Java object, you can do ajc.CallStatic<AndroidJavaObject> regardless of what type of object it is.

AndroidJavaObject.Get

This is the most basic. Just specify the field's name like when you are making custom editor and trying to drill down into SerializedObject. Here's some simple usage.

This is a Java class.

public class DeviceAudioInformation
{
    int nativeSamplingRate;
    int optimalBufferSize;
    boolean lowLatencyFeature;
    boolean proAudioFeature;
    AudioDeviceInfo[] outputDevices; //difficult
}

Meanwhile at C#, supposed you have got AndroidJavaObject that you know is of type DeviceAudioInformation in Java.

this.nativeSamplingRate = jo.Get<int>("nativeSamplingRate");
this.optimalBufferSize = jo.Get<int>("optimalBufferSize");
this.lowLatencyFeature = jo.Get<bool>("lowLatencyFeature");
this.proAudioFeature = jo.Get<bool>("proAudioFeature");

Yeah, <bool> works for boolean too. But what about that last one? AudioDeviceInfo[]

This AudioDeviceInfo is a class from Google. Not only that, it is an array. How to go further? My objective is to not only iterate on them, I want to call instance method on each to extract further data. That AudioDeviceInfo got .getType() I want to call from C#, for example.

https://developer.android.com/reference/android/media/AudioDeviceInfo.html

How to get into array : JNI manually

First, you need to realize you can  AndroidJavaObject.Get<AndroidJavaObject> to get an another object, effectively drilling down recursively.

AndroidJavaObject outputDevicesJo = jo.Get<AndroidJavaObject>("outputDevices");

But to get into an array, we are entering the land of IntPtr. So with AndroidJavaObject and the like Unity is managing pointer to Java things for us. Now we have to do it.

The first step to manual JNI is GetRawObject. Then we have access to various AndroidJNI static helper methods, like getting array length.

IntPtr outputDevicesRaw = outputDevicesJo.GetRawObject();
int outputDeviceAmount = AndroidJNI.GetArrayLength(outputDevicesRaw);

Any method from AndroidJNI can hard crash Unity if you are doing it wrong! Usually with SIGSEGV, as it is doing pointer gymnastic and if it lands somewhere strange then you get that.

With length, and AndroidJNI.GetObjectArrayElement you can now get an another IntPtr representing an item in that array. (If the items is of primitive type, you can use GetIntArrayElement for example to end your journey into Java.)

for (int i = 0; i < outputDeviceAmount; i++)
{
    IntPtr outputDevice = AndroidJNI.GetObjectArrayElement(outputDevicesRaw, i);
}

You can feel that after you left AndroidJavaObject to IntPtr, you cannot go back to AndroidJavaObject anymore. It's always IntPtr from this point.

How to call an instance method on JNI-ed object

You will need a "method ID". And to get that you will need "class ID" too. This feel just like when we are doing C# reflection where we need the class type then MethodInfo of the thing we want to use.

AndroidJNI.FindClass  can create a class ID out of thin air, but it is difficult to get the argument right. If you are up to the challenge then ...

https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html#wp16031

But I am not, so an easier way is to ask the class from the object we have already got.

IntPtr audioDeviceInfoClass = AndroidJNI.GetObjectClass(outputDevice);

Next is the method ID. AndroidJNI.GetMethodID is again, can create that out of a class ID and something something. But I have never get the argument right again lol. If you are up to the challenge again however ...

https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html#wp16660

But I am not going to, so instead use AndroidJNIHelper.GetMethodID. By using the helper, you can use just the method name and class ID.

IntPtr getTypeMethod = AndroidJNIHelper.GetMethodID(audioDeviceInfoClass, "getType");

Finally you can call the method with a combination of instance and method ID, just like C# reflection. In all, it would look like this.

for (int i = 0; i < outputDeviceAmount; i++)
{
    IntPtr outputDevice = AndroidJNI.GetObjectArrayElement(outputDevicesRaw, i);
    IntPtr audioDeviceInfoClass = AndroidJNI.GetObjectClass(outputDevice);
    IntPtr getTypeMethod = AndroidJNIHelper.GetMethodID(audioDeviceInfoClass, "getType");
    int type = AndroidJNI.CallIntMethod(outputDevice, getTypeMethod, new jvalue[] { });
    this.outputDevices[i] = (AndroidAudioDeviceType) type;
}

I have successfully iterate over an array of Java objects, and moreover call an instance method on each of them and keep the return value. I think this is enough to drill down any kind of Java object already.