ipc/android: Stop runtime service when no clients connected

This commit is contained in:
Jarvis Huang 2021-10-27 12:10:11 +08:00 committed by Ryan Pavlik
parent ec537eb3aa
commit 6a61ed5695
9 changed files with 192 additions and 98 deletions
src/xrt
auxiliary/android/src/main/java/org/freedesktop/monado/auxiliary
ipc/android
targets
android_common/src/main/java/org/freedesktop/monado/android_common
service-lib

View file

@ -24,7 +24,7 @@ interface IServiceNotification {
* Create and return a notification (creating the channel if applicable) that can be used in * Create and return a notification (creating the channel if applicable) that can be used in
* {@code Service#startForeground()} * {@code Service#startForeground()}
*/ */
fun buildNotification(context: Context, pendingShutdownIntent: PendingIntent): Notification fun buildNotification(context: Context): Notification
/** /**
* Return the notification ID to use * Return the notification ID to use

View file

@ -21,13 +21,11 @@ android {
// Single point of truth for these names. // Single point of truth for these names.
def serviceAction = "org.freedesktop.monado.ipc.CONNECT" def serviceAction = "org.freedesktop.monado.ipc.CONNECT"
def shutdownAction = "org.freedesktop.monado.ipc.SHUTDOWN"
manifestPlaceholders = [ manifestPlaceholders = [
serviceActionName : serviceAction, serviceActionName : serviceAction,
shutdownActionName: shutdownAction
] ]
buildConfigField("String", "SERVICE_ACTION", "\"${serviceAction}\"") buildConfigField("String", "SERVICE_ACTION", "\"${serviceAction}\"")
buildConfigField("String", "SHUTDOWN_ACTION", "\"${shutdownAction}\"") buildConfigField("Long", "WATCHDOG_TIMEOUT_MILLISECONDS", "1500L")
} }
buildTypes { buildTypes {

View file

@ -15,7 +15,6 @@
<intent-filter> <intent-filter>
<action android:name="${serviceActionName}" /> <action android:name="${serviceActionName}" />
<action android:name="${shutdownActionName}" />
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View file

@ -78,10 +78,6 @@ public class Client implements ServiceConnection {
* Context of the runtime package * Context of the runtime package
*/ */
private Context runtimePackageContext = null; private Context runtimePackageContext = null;
/**
* Intent for connecting to service
*/
private Intent intent = null;
/** /**
* Controll system ui visibility * Controll system ui visibility
*/ */
@ -103,10 +99,6 @@ public class Client implements ServiceConnection {
if (context != null) { if (context != null) {
context.unbindService(this); context.unbindService(this);
} }
if (intent != null) {
context.stopService(intent);
}
intent = null;
if (fd != null) { if (fd != null) {
try { try {
@ -255,11 +247,6 @@ public class Client implements ServiceConnection {
Intent intent = new Intent(BuildConfig.SERVICE_ACTION) Intent intent = new Intent(BuildConfig.SERVICE_ACTION)
.setPackage(packageName); .setPackage(packageName);
if (context.startForegroundService(intent) == null) {
Log.e(TAG, "startForegroundService: Service " + intent.toString() + " does not exist!");
return false;
}
if (!bindService(context, intent)) { if (!bindService(context, intent)) {
Log.e(TAG, Log.e(TAG,
"bindService: Service " + intent.toString() + " could not be found to bind!"); "bindService: Service " + intent.toString() + " could not be found to bind!");

View file

@ -42,11 +42,6 @@ public class MonadoImpl extends IMonado.Stub {
System.loadLibrary("monado-service"); System.loadLibrary("monado-service");
} }
private final Thread compositorThread = new Thread(
this::threadEntry,
"CompositorThread");
private boolean started = false;
private SurfaceManager surfaceManager; private SurfaceManager surfaceManager;
public MonadoImpl(@NonNull SurfaceManager surfaceManager) { public MonadoImpl(@NonNull SurfaceManager surfaceManager) {
@ -55,7 +50,7 @@ public class MonadoImpl extends IMonado.Stub {
@Override @Override
public void surfaceCreated(@NonNull SurfaceHolder holder) { public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated"); Log.i(TAG, "surfaceCreated");
nativeAppSurface(holder.getSurface()); passAppSurface(holder.getSurface());
} }
@Override @Override
@ -68,20 +63,8 @@ public class MonadoImpl extends IMonado.Stub {
}); });
} }
private void launchThreadIfNeeded() {
synchronized (compositorThread) {
if (!started) {
compositorThread.start();
nativeWaitForServerStartup();
started = true;
}
}
}
@Override @Override
public void connect(@NotNull ParcelFileDescriptor parcelFileDescriptor) { public void connect(@NotNull ParcelFileDescriptor parcelFileDescriptor) {
/// @todo launch this thread earlier/elsewhere
launchThreadIfNeeded();
int fd = parcelFileDescriptor.getFd(); int fd = parcelFileDescriptor.getFd();
Log.i(TAG, "connect: given fd " + fd); Log.i(TAG, "connect: given fd " + fd);
if (nativeAddClient(fd) != 0) { if (nativeAddClient(fd) != 0) {
@ -105,6 +88,12 @@ public class MonadoImpl extends IMonado.Stub {
return; return;
} }
nativeAppSurface(surface); nativeAppSurface(surface);
startServerIfNeeded();
}
private void startServerIfNeeded() {
nativeStartServer();
nativeWaitForServerStartup();
} }
@Override @Override
@ -119,17 +108,16 @@ public class MonadoImpl extends IMonado.Stub {
return surfaceManager.canDrawOverlays(); return surfaceManager.canDrawOverlays();
} }
private void threadEntry() { public void shutdown() {
Log.i(TAG, "threadEntry"); Log.i(TAG, "shutdown");
nativeThreadEntry(); nativeShutdownServer();
Log.i(TAG, "native thread has exited");
} }
/** /**
* Native thread entry point. * Native method that starts server.
*/ */
@SuppressWarnings("JavaJniMissingFunction") @SuppressWarnings("JavaJniMissingFunction")
private native void nativeThreadEntry(); private native void nativeStartServer();
/** /**
* Native method that waits until the server reports that it is, in fact, started up. * Native method that waits until the server reports that it is, in fact, started up.
@ -167,4 +155,12 @@ public class MonadoImpl extends IMonado.Stub {
*/ */
@SuppressWarnings("JavaJniMissingFunction") @SuppressWarnings("JavaJniMissingFunction")
private native int nativeAddClient(int fd); private native int nativeAddClient(int fd);
/**
* Native method that handles shutdown server.
*
* @return 0 on success; -1 means that server didn't start.
*/
@SuppressWarnings("JavaJniMissingFunction")
private native int nativeShutdownServer();
} }

View file

@ -8,7 +8,6 @@
*/ */
package org.freedesktop.monado.ipc package org.freedesktop.monado.ipc
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
@ -25,10 +24,10 @@ import javax.inject.Inject
* This is needed so that the APK can expose the binder service implemented in MonadoImpl. * This is needed so that the APK can expose the binder service implemented in MonadoImpl.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MonadoService : Service() { class MonadoService : Service(), Watchdog.ShutdownListener {
private val binder: MonadoImpl by lazy { private lateinit var binder: MonadoImpl
MonadoImpl(surfaceManager)
} private val watchdog = Watchdog(BuildConfig.WATCHDOG_TIMEOUT_MILLISECONDS, this)
@Inject @Inject
lateinit var serviceNotification: IServiceNotification lateinit var serviceNotification: IServiceNotification
@ -39,44 +38,36 @@ class MonadoService : Service() {
super.onCreate() super.onCreate()
surfaceManager = SurfaceManager(this) surfaceManager = SurfaceManager(this)
binder = MonadoImpl(surfaceManager)
watchdog.startMonitor()
// start the service so it could be foregrounded
startService(Intent(this, javaClass))
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Log.d(TAG, "onDestroy")
binder.shutdown();
watchdog.stopMonitor()
surfaceManager.destroySurface() surfaceManager.destroySurface()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand") Log.d(TAG, "onStartCommand")
// if this isn't a restart handleStart()
if (intent != null) {
when (intent.action) {
BuildConfig.SERVICE_ACTION -> handleStart()
BuildConfig.SHUTDOWN_ACTION -> handleShutdown()
}
}
return START_STICKY return START_STICKY
} }
private fun handleShutdown() {
stopForeground(true)
stopSelf()
}
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
Log.d(TAG, "onBind");
watchdog.onClientConnected()
return binder return binder
} }
private fun handleStart() { private fun handleStart() {
val pendingShutdownIntent = PendingIntent.getForegroundService( val notification = serviceNotification.buildNotification(this)
this,
0,
Intent(BuildConfig.SHUTDOWN_ACTION).setPackage(packageName),
0
)
val notification = serviceNotification.buildNotification(this, pendingShutdownIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground( startForeground(
@ -92,6 +83,27 @@ class MonadoService : Service() {
} }
} }
override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind");
watchdog.onClientDisconnected()
return true;
}
override fun onRebind(intent: Intent?) {
Log.d(TAG, "onRebind");
watchdog.onClientConnected()
}
override fun onPrepareShutdown() {
Log.d(TAG, "onPrepareShutdown")
}
override fun onShutdown() {
Log.d(TAG, "onShutdown")
stopForeground(true)
stopSelf()
}
companion object { companion object {
private const val TAG = "MonadoService" private const val TAG = "MonadoService"
} }

View file

@ -0,0 +1,78 @@
// Copyright 2021, Qualcomm Innovation Center, Inc.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Monitor client connections.
* @author Jarvis Huang
* @ingroup ipc_android
*/
package org.freedesktop.monado.ipc
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
import java.util.concurrent.atomic.AtomicInteger
/**
* Client watchdog, to determine whether runtime service should be stopped.
*/
class Watchdog(
private val shutdownDelayMilliseconds: Long,
private val shutdownListener: ShutdownListener
) {
/**
* Interface definition for callbacks to be invoked when there's no client connected. Noted that
* all the callbacks run on background thread.
*/
interface ShutdownListener {
/**
* Callback to be invoked when last client disconnected.
*/
fun onPrepareShutdown()
/**
* Callback to be invoked when shutdown delay ended and there's no new client connected.
*/
fun onShutdown()
}
private val clientCount = AtomicInteger(0)
private lateinit var shutdownHandler: Handler
private lateinit var shutdownThread: HandlerThread
fun startMonitor() {
shutdownThread = HandlerThread("monado-client-watchdog")
shutdownThread.start()
shutdownHandler = object : Handler(shutdownThread.looper) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SHUTDOWN -> if (clientCount.get() == 0) {
shutdownListener.onShutdown()
}
}
}
}
}
fun stopMonitor() {
shutdownThread.quitSafely()
}
fun onClientConnected() {
clientCount.incrementAndGet()
shutdownHandler.removeMessages(MSG_SHUTDOWN)
}
fun onClientDisconnected() {
if (clientCount.decrementAndGet() == 0) {
shutdownListener.onPrepareShutdown()
shutdownHandler.sendEmptyMessageDelayed(MSG_SHUTDOWN, shutdownDelayMilliseconds)
}
}
companion object {
private const val MSG_SHUTDOWN = 1000
}
}

View file

@ -70,14 +70,9 @@ class ServiceNotificationImpl @Inject constructor() : IServiceNotification {
* Create and return a notification (creating the channel if applicable) that can be used in * Create and return a notification (creating the channel if applicable) that can be used in
* {@code Service#startForeground()} * {@code Service#startForeground()}
*/ */
override fun buildNotification(context: Context, pendingShutdownIntent: PendingIntent): Notification { override fun buildNotification(context: Context): Notification {
createChannel(context) createChannel(context)
val action = Notification.Action.Builder(
Icon.createWithResource(context, R.drawable.ic_feathericons_x),
context.getString(R.string.notifExitRuntime),
pendingShutdownIntent)
.build()
// Make a notification for our foreground service // Make a notification for our foreground service
// When selected it will open the "About" activity // When selected it will open the "About" activity
val builder = makeNotificationBuilder(context) val builder = makeNotificationBuilder(context)
@ -87,7 +82,6 @@ class ServiceNotificationImpl @Inject constructor() : IServiceNotification {
R.string.notif_text, R.string.notif_text,
nameAndLogoProvider.getLocalizedRuntimeName())) nameAndLogoProvider.getLocalizedRuntimeName()))
.setShowWhen(false) .setShowWhen(false)
.addAction(action)
.setContentIntent(uiProvider.makeAboutActivityPendingIntent()) .setContentIntent(uiProvider.makeAboutActivityPendingIntent())
// Notification icon is optional // Notification icon is optional

View file

@ -20,45 +20,33 @@
#include <android/native_window_jni.h> #include <android/native_window_jni.h>
#include "android/android_globals.h" #include "android/android_globals.h"
#include <memory>
#include <thread> #include <thread>
using wrap::android::view::Surface; using wrap::android::view::Surface;
namespace { namespace {
struct Singleton struct IpcServerHelper
{ {
public: public:
static Singleton & IpcServerHelper() {}
instance()
{
static Singleton singleton{};
return singleton;
}
void void
waitForStartupComplete() waitForStartupComplete()
{ {
std::unique_lock<std::mutex> lock{running_mutex}; std::unique_lock<std::mutex> lock{running_mutex};
running_cond.wait(lock, [&]() { return this->startup_complete; }); running_cond.wait(lock, [&]() { return this->startup_complete; });
} }
//! static trampoline for the startup complete callback
static void
signalStartupComplete()
{
instance().signalStartupCompleteNonstatic();
}
private:
void void
signalStartupCompleteNonstatic() signalStartupComplete()
{ {
std::unique_lock<std::mutex> lock{running_mutex}; std::unique_lock<std::mutex> lock{running_mutex};
startup_complete = true; startup_complete = true;
running_cond.notify_all(); running_cond.notify_all();
} }
Singleton() {}
private:
//! Mutex for starting thread //! Mutex for starting thread
std::mutex running_mutex; std::mutex running_mutex;
@ -69,27 +57,44 @@ private:
} // namespace } // namespace
static struct ipc_server *server = NULL; static struct ipc_server *server = NULL;
static IpcServerHelper *helper = nullptr;
static std::unique_ptr<std::thread> server_thread{};
static std::mutex server_thread_mutex;
static void static void
signalStartupCompleteTrampoline(void *data) signalStartupCompleteTrampoline(void *data)
{ {
static_cast<Singleton *>(data)->signalStartupComplete(); static_cast<IpcServerHelper *>(data)->signalStartupComplete();
} }
extern "C" void extern "C" void
Java_org_freedesktop_monado_ipc_MonadoImpl_nativeThreadEntry(JNIEnv *env, jobject thiz) Java_org_freedesktop_monado_ipc_MonadoImpl_nativeStartServer(JNIEnv *env, jobject thiz)
{ {
jni::init(env); jni::init(env);
jni::Object monadoImpl(thiz); jni::Object monadoImpl(thiz);
U_LOG_D("service: Called nativeThreadEntry"); U_LOG_D("service: Called nativeThreadEntry");
auto &singleton = Singleton::instance();
ipc_server_main_android(&server, signalStartupCompleteTrampoline, &singleton); {
// Start IPC server
std::unique_lock lock(server_thread_mutex);
if (!server && !server_thread) {
helper = new IpcServerHelper();
server_thread = std::make_unique<std::thread>(
[]() { ipc_server_main_android(&server, signalStartupCompleteTrampoline, helper); });
helper->waitForStartupComplete();
}
}
} }
extern "C" JNIEXPORT void JNICALL extern "C" JNIEXPORT void JNICALL
Java_org_freedesktop_monado_ipc_MonadoImpl_nativeWaitForServerStartup(JNIEnv *env, jobject thiz) Java_org_freedesktop_monado_ipc_MonadoImpl_nativeWaitForServerStartup(JNIEnv *env, jobject thiz)
{ {
Singleton::instance().waitForStartupComplete(); if (server == nullptr) {
// Should not happen.
U_LOG_E("service: nativeWaitForServerStartup called before service started up!");
return;
}
helper->waitForStartupComplete();
} }
extern "C" JNIEXPORT jint JNICALL extern "C" JNIEXPORT jint JNICALL
@ -118,3 +123,28 @@ Java_org_freedesktop_monado_ipc_MonadoImpl_nativeAppSurface(JNIEnv *env, jobject
android_globals_store_window((struct _ANativeWindow *)nativeWindow); android_globals_store_window((struct _ANativeWindow *)nativeWindow);
U_LOG_D("Stored ANativeWindow: %p", (void *)nativeWindow); U_LOG_D("Stored ANativeWindow: %p", (void *)nativeWindow);
} }
extern "C" JNIEXPORT jint JNICALL
Java_org_freedesktop_monado_ipc_MonadoImpl_nativeShutdownServer(JNIEnv *env, jobject thiz)
{
jni::init(env);
jni::Object monadoImpl(thiz);
if (server == nullptr || !server_thread) {
// Should not happen.
U_LOG_E("service: nativeShutdownServer called before service started up!");
return -1;
}
{
// Wait until IPC server stop
std::unique_lock lock(server_thread_mutex);
ipc_server_handle_shutdown_signal(server);
server_thread->join();
server_thread.reset(nullptr);
delete helper;
helper = nullptr;
server = NULL;
}
return 0;
}