diff --git a/settings.gradle b/settings.gradle index b286e35e6..891daf3ba 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,5 @@ rootProject.name = 'monado' -include ":src:xrt:auxiliary:android" +include ':src:xrt:auxiliary:android' +include ':src:xrt:ipc_android' diff --git a/src/xrt/ipc_android/build.gradle b/src/xrt/ipc_android/build.gradle new file mode 100644 index 000000000..fad3a0cae --- /dev/null +++ b/src/xrt/ipc_android/build.gradle @@ -0,0 +1,33 @@ +// Copyright 2020, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 + +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.sharedTargetSdk + buildToolsVersion '30.0.2' + + + defaultConfig { + minSdkVersion 24 + targetSdkVersion project.sharedTargetSdk + } + + buildTypes { + release { + minifyEnabled false + // Gradle plugin produces proguard-android-optimize.txt from @Keep annotations + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation project(':src:xrt:auxiliary:android') + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.annotation:annotation:1.1.0' +} diff --git a/src/xrt/ipc_android/proguard-rules.pro b/src/xrt/ipc_android/proguard-rules.pro new file mode 100644 index 000000000..686f8abdb --- /dev/null +++ b/src/xrt/ipc_android/proguard-rules.pro @@ -0,0 +1,4 @@ +# Copyright 2020, Collabora, Ltd. +# SPDX-License-Identifier: BSL-1.0 +# see http://developer.android.com/guide/developing/tools/proguard.html +# Trying to keep most of them in source code annotations and let Gradle do the work diff --git a/src/xrt/ipc_android/src/main/AndroidManifest.xml b/src/xrt/ipc_android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4d981e3e1 --- /dev/null +++ b/src/xrt/ipc_android/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/xrt/ipc_android/src/main/aidl/org/freedesktop/monado/ipc/IMonado.aidl b/src/xrt/ipc_android/src/main/aidl/org/freedesktop/monado/ipc/IMonado.aidl new file mode 100644 index 000000000..f1dcfbbf4 --- /dev/null +++ b/src/xrt/ipc_android/src/main/aidl/org/freedesktop/monado/ipc/IMonado.aidl @@ -0,0 +1,25 @@ +// Copyright 2020, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Interface to bootstrap the Monado IPC connection. + * @author Ryan Pavlik + * @ingroup aux_android_java + */ + +package org.freedesktop.monado.ipc; + +import android.os.ParcelFileDescriptor; +import android.view.Surface; + +interface IMonado { + /*! + * Pass one side of the socket pair to the service to set up the IPC. + */ + void connect(in ParcelFileDescriptor parcelFileDescriptor); + + /*! + * Provide the surface we inject into the activity, back to the service. + */ + void passAppSurface(in Surface surface); +} diff --git a/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/Client.java b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/Client.java new file mode 100644 index 000000000..5d75546f3 --- /dev/null +++ b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/Client.java @@ -0,0 +1,147 @@ +// Copyright 2020, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Implementation of the Monado AIDL server + * @author Ryan Pavlik + */ + + +package org.freedesktop.monado.ipc; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.Keep; + +import java.io.IOException; + +/** + * Provides the client-side code to initiate connection to Monado IPC service. + *

+ * This class will get loaded into the OpenXR client application by our native code. + */ +@Keep +public class Client implements ServiceConnection { + private static final String TAG = "monado-ipc-client"; + + /** + * Context provided by app. + */ + private Context context; + + /** + * Pointer to local IPC proxy: calling methods on it automatically transports arguments across binder IPC. + *

+ * May be null! + */ + public IMonado monado; + + /** + * "Our" side of the socket pair - the other side is sent to the server automatically on connection. + */ + public ParcelFileDescriptor fd; + + /** + * Indicates that we tried to connect but failed. + *

+ * Used to distinguish a "not yet fully connected" null monado member from a "tried and failed" + * null monado member. + */ + public boolean failed = false; + + /** + * Bind to the Monado IPC service - this asynchronously starts connecting (and launching the + * service if it's not already running) + *

+ * The IPC client code on Android should load this class (from the right package), instantiate + * this class, and call this method. + * + * @param context_ Context to use to make the connection. (We get the application context + * from it.) + * @param packageName The package name containing the Monado runtime. The caller is guaranteed + * to know this because it had to load this class from that package. + * (Often "org.freedesktop.monado.openxr.out_of_process" for now, at least) + * @todo how to get the right package name here? Do we have to go so far as to re-enumerate ourselves? + *

+ * Various builds, variants, etc. will have different package names, but we must specify the + * package name explicitly to avoid violating security restrictions. + */ + public void bind(Context context_, String packageName) { + context = context_.getApplicationContext(); + if (context == null) { + // in case app context returned null + context = context_; + } + context.bindService( + new Intent("org.freedesktop.monado.CONNECT") + .setPackage(packageName), + this, Context.BIND_AUTO_CREATE); + // does not bind right away! This takes some time. + } + + /** + * Some on-failure cleanup. + */ + private void handleFailure() { + failed = true; + if (context != null) context.unbindService(this); + monado = null; + } + + /** + * Handle the asynchronous connection of the binder IPC. + *

+ * This sets up the class member `monado`, as well as the member `fd`. It calls + * `IMonado.connect()` automatically. The client still needs to call `IMonado.passAppSurface()` + * on `monado`. + * + * @param name should match the intent above, but not used. + * @param service the associated service, which we cast in this function. + */ + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + monado = IMonado.Stub.asInterface(service); + ParcelFileDescriptor theirs; + try { + ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair(); + fd = fds[0]; + theirs = fds[1]; + } catch (IOException e) { + e.printStackTrace(); + Log.e(TAG, "could not create socket pair: " + e.toString()); + handleFailure(); + return; + } + try { + monado.connect(theirs); + } catch (RemoteException e) { + e.printStackTrace(); + Log.e(TAG, "could not call IMonado.connect: " + e.toString()); + handleFailure(); + } + } + + /** + * Handle asynchronous disconnect. + * + * @param name should match the intent above, but not used. + */ + @Override + public void onServiceDisconnected(ComponentName name) { + monado = null; + } + + /* + * @todo do we need to watch for a disconnect here? + * https://stackoverflow.com/questions/18078914/notify-an-android-service-when-a-bound-client-disconnects + * + * Our existing native disconnect handling might be sufficient. + */ +} diff --git a/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java new file mode 100644 index 000000000..341f212ca --- /dev/null +++ b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java @@ -0,0 +1,72 @@ +// Copyright 2020, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Implementation of the Monado AIDL server + * @author Ryan Pavlik + */ + + +package org.freedesktop.monado.ipc; + +import android.os.ParcelFileDescriptor; +import android.view.Surface; + +import androidx.annotation.Keep; + +import org.freedesktop.monado.ipc.IMonado.Stub; + +/** + * Java implementation of the IMonado IPC interface. + * + * All this does is delegate calls to native JNI implementations + */ +@Keep +public class MonadoImpl extends IMonado.Stub { + + public void connect(ParcelFileDescriptor parcelFileDescriptor) { + nativeAddClient(parcelFileDescriptor); + } + + public void passAppSurface(Surface surface) { + nativeAppSurface(surface); + } + + /** + * Native handling of receiving a surface: should convert it to an ANativeWindow then do stuff + * with it. + * + * Ignore Android Studio complaining that this function is missing: it is not, it is just in a + * different module. See `src/xrt/targets/service-lib/lib.cpp` for the implementation. + * (Ignore the warning saying that file isn't included in the build: it is, Android Studio + * is just confused.) + * + * @param surface + * @todo figure out a good way to make the MonadoImpl pointer a client ID + */ + private native void nativeAppSurface(Surface surface); + + /** + * Native handling of receiving an FD for a new client: the FD should be used to start up the + * rest of the native IPC code on that socket. + * + * This is essentially the entry point for the monado service on Android: if it's already + * running, this will be called in it. If it's not already running, a process will be created, + * and this will be the first native code executed in that process. + * + * Ignore Android Studio complaining that this function is missing: it is not, it is just in a + * different module. See `src/xrt/targets/service-lib/lib.cpp` for the implementation. + * (Ignore the warning saying that file isn't included in the build: it is, Android Studio + * is just confused.) + * + * @param surface + * @todo figure out a good way to make the MonadoImpl pointer a client ID + */ + private native void nativeAddClient(ParcelFileDescriptor parcelFileDescriptor); + + static { + // Load the shared library with the native parts of this class + // This is the service-lib target. + System.loadLibrary("monado-service"); + } +} diff --git a/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoService.java b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoService.java new file mode 100644 index 000000000..d29feec95 --- /dev/null +++ b/src/xrt/ipc_android/src/main/java/org/freedesktop/monado/ipc/MonadoService.java @@ -0,0 +1,31 @@ +// Copyright 2020, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Service implementation for exposing IMonado. + * @author Ryan Pavlik + */ + + +package org.freedesktop.monado.ipc; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +import androidx.annotation.Nullable; + +/** + * Minimal implementation of a Service. + * + * This is needed so that the APK can expose the binder service implemented in MonadoImpl. + */ +public class MonadoService extends Service { + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return new MonadoImpl(); + } + +} diff --git a/src/xrt/ipc_android/src/main/res/values/strings.xml b/src/xrt/ipc_android/src/main/res/values/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/src/xrt/ipc_android/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + +