Skip to content

Commit

Permalink
Android: Add classes to embed QML into native Android
Browse files Browse the repository at this point in the history
Add classes that make it possible to add QML as a View into
a native Android app:

QtView: Base class for QtQuickView, handles non-Quick dependent
operations. In essence a Java ViewGroup class which loads a
QWindow and embeds it into itself.

QtEmbeddedLoader: Extends QtLoader for embedded case, creates the
embedded version of QtActivityDelegate and provides an embedded-specific
path to loading Qt libraries (Mostly just allows users to set the name
of the main lib)

QtAndroidWindowEmbedding namespace: Deals with calls from
QtEmbeddedDelegate to create/destroy QWindow and from QtView to
show the window.

Take the QtEmbeddedDelegate introduced in an earlier commit
into use, and add functionality for loading QWindows for
QtViews and managing QtViews into it.

Add a factory for creating instances of QtEmbeddedDelegate.
The factory holds a map of QtEmbeddedDelegate objects and
creates them, with the Activity as the key. This is to make
it so that the same delegate can be used by multiple views
which share the same Context.

Known issues left:
* keyboard focus not working, as with other child windows

Pick-to: 6.7
Task-number: QTBUG-118872
Change-Id: I94a5f9b4f904c05cc6368cf20f273fcf10d31f17
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
  • Loading branch information
Tinja Paavoseppä committed Jan 30, 2024
1 parent b3441b4 commit 702c420
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/android/jar/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ set(java_sources
src/org/qtproject/qt/android/QtWindow.java
src/org/qtproject/qt/android/QtActivityDelegateBase.java
src/org/qtproject/qt/android/QtEmbeddedDelegate.java
src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java
src/org/qtproject/qt/android/QtEmbeddedLoader.java
src/org/qtproject/qt/android/QtView.java
)

qt_internal_add_jar(Qt${QtBase_VERSION_MAJOR}Android
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

package org.qtproject.qt.android;
Expand All @@ -22,8 +22,14 @@
import java.util.HashMap;

class QtEmbeddedDelegate extends QtActivityDelegateBase implements QtNative.AppStateDetailsListener {
// TODO simplistic implementation with one QtView, expand to support multiple views QTBUG-117649
private QtView m_view;
private long m_rootWindowRef = 0L;
private QtNative.ApplicationStateDetails m_stateDetails;

private static native void createRootWindow(View rootView);
static native void deleteWindow(long windowReference);

public QtEmbeddedDelegate(Activity context) {
super(context);

Expand Down Expand Up @@ -71,6 +77,7 @@ public void onActivityDestroyed(Activity activity) {
if (m_activity == activity && m_stateDetails.isStarted) {
m_activity.getApplication().unregisterActivityLifecycleCallbacks(this);
QtNative.unregisterAppStateListener(QtEmbeddedDelegate.this);
QtEmbeddedDelegateFactory.remove(m_activity);
QtNative.terminateQt();
QtNative.setActivity(null);
QtNative.getQtThread().exit();
Expand All @@ -89,6 +96,8 @@ public void onAppStateDetailsChanged(QtNative.ApplicationStateDetails details) {
QtDisplayManager.setApplicationDisplayMetrics(m_activity,
metrics.widthPixels,
metrics.heightPixels);
if (m_view != null)
createRootWindow(m_view);
});
}
}
Expand All @@ -111,11 +120,30 @@ QtAccessibilityDelegate createAccessibilityDelegate()
@Override
QtLayout getQtLayout()
{
// TODO could probably use QtView here when it's added?
return null;
// TODO verify if returning m_view here works, this is used by the androidjniinput
// when e.g. showing a keyboard, so depends on getting the keyboard focus working
// QTBUG-118873
return m_view;
}

public void queueLoadWindow()
{
if (m_stateDetails.nativePluginIntegrationReady) {
createRootWindow(m_view);
}
}

void setView(QtView view) {
m_view = view;
}

public void setRootWindowRef(long ref) {
m_rootWindowRef = ref;
}

public void onDestroy() {
// TODO delete the window once it's added
if (m_rootWindowRef != 0L)
deleteWindow(m_rootWindowRef);
m_rootWindowRef = 0L;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

package org.qtproject.qt.android;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import java.util.HashMap;

class QtEmbeddedDelegateFactory {
private static final HashMap<Activity, QtEmbeddedDelegate> m_delegates = new HashMap<>();
private static final Object m_delegateLock = new Object();

@UsedFromNativeCode
public static QtActivityDelegateBase getActivityDelegate(Activity activity) {
synchronized (m_delegateLock) {
return m_delegates.get(activity);
}
}

public static QtEmbeddedDelegate create(Activity activity) {
synchronized (m_delegateLock) {
if (!m_delegates.containsKey(activity))
m_delegates.put(activity, new QtEmbeddedDelegate(activity));

return m_delegates.get(activity);
}
}

public static void remove(Activity activity) {
synchronized (m_delegateLock) {
m_delegates.remove(activity);
}
}
}
48 changes: 48 additions & 0 deletions src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

package org.qtproject.qt.android;

import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.SurfaceView;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

import dalvik.system.DexClassLoader;
import android.content.res.Resources;

class QtEmbeddedLoader extends QtLoader {
private static final String TAG = "QtEmbeddedLoader";

public QtEmbeddedLoader(Context context) {
super(new ContextWrapper(context));
// TODO Service context handling QTBUG-118874
}

@Override
protected void finish() {
// Called when loading fails - clear the delegate to make sure we don't hold reference
// to the embedding Context
QtEmbeddedDelegateFactory.remove((Activity)m_context.getBaseContext());
}
}
130 changes: 130 additions & 0 deletions src/android/jar/src/org/qtproject/qt/android/QtView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

package org.qtproject.qt.android;

import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;

import java.security.InvalidParameterException;
import java.util.ArrayList;

// TODO this should not need to extend QtLayout, a simple FrameLayout/ViewGroup should do
// QTBUG-121516
// Base class for embedding QWindow into native Android view hierarchy. Extend to implement
// the creation of appropriate window to embed.
abstract class QtView extends QtLayout {
private final static String TAG = "QtView";

public interface QtWindowListener {
// Called when the QWindow has been created and it's Java counterpart embedded into
// QtView
void onQtWindowLoaded();
}

protected QtWindow m_window;
protected long m_windowReference;
protected QtWindowListener m_windowListener;
protected QtEmbeddedDelegate m_delegate;
// Implement in subclass to handle the creation of the QWindow and its parent container.
// TODO could we take care of the parent window creation and parenting outside of the
// window creation method to simplify things if user would extend this? Preferably without
// too much JNI back and forth. Related to parent window creation, so handle with QTBUG-121511.
abstract protected void createWindow(long parentWindowRef);

private static native void setWindowVisible(long windowReference, boolean visible);

/**
* Create QtView for embedding a QWindow. Instantiating a QtView will load the Qt libraries
* if they have not already been loaded, including the app library specified by appName, and
* starting the said Qt app.
* @param context the hosting Context
* @param appLibName the name of the Qt app library to load and start. This corresponds to the
target name set in Qt app's CMakeLists.txt
**/
public QtView(Context context, String appLibName) throws InvalidParameterException {
super(context);
if (appLibName == null || appLibName.isEmpty()) {
throw new InvalidParameterException("QtView: argument 'appLibName' may not be empty "+
"or null");
}

QtEmbeddedLoader loader = new QtEmbeddedLoader(context);
m_delegate = QtEmbeddedDelegateFactory.create((Activity)context);
loader.setMainLibraryName(appLibName);
loader.loadQtLibraries();
// Start Native Qt application
m_delegate.startNativeApplication(loader.getApplicationParameters(),
loader.getMainLibraryPath());
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
m_delegate.setView(this);
m_delegate.queueLoadWindow();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
destroyWindow();
m_delegate.setView(null);
}

public void setQtWindowListener(QtWindowListener listener) {
m_windowListener = listener;
}

void setWindowReference(long windowReference) {
m_windowReference = windowReference;
}

long windowReference() {
return m_windowReference;
}

// Set the visibility of the underlying QWindow. If visible is true, showNormal() is called.
// If false, the window is hidden.
void setWindowVisible(boolean visible) {
if (m_windowReference != 0L)
setWindowVisible(m_windowReference, true);
}

// Called from Qt when the QWindow has been created.
// window - the Java QtWindow of the created QAndroidPlatformWindow, to embed into the QtView
// viewReference - the reference to the created QQuickView
void addQtWindow(QtWindow window, long viewReference, long parentWindowRef) {
setWindowReference(viewReference);
m_delegate.setRootWindowRef(parentWindowRef);
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
m_window = window;
m_window.getLayout().setLayoutParams(new QtLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
addView(m_window.getLayout(), 0);
// Call show window + parent
setWindowVisible(true);
if (m_windowListener != null)
m_windowListener.onQtWindowLoaded();
}
});
}

// Destroy the underlying QWindow
void destroyWindow() {
if (m_windowReference != 0L)
QtEmbeddedDelegate.deleteWindow(m_windowReference);
m_windowReference = 0L;
}
}
3 changes: 2 additions & 1 deletion src/android/jar/src/org/qtproject/qt/android/QtWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ public void setGeometry(final int x, final int y, final int w, final int h)
QtNative.runAction(new Runnable() {
@Override
public void run() {
m_layout.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
if (m_layout.getContext() instanceof QtActivityBase)
m_layout.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
}
});
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/platforms/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin
qandroidplatformtheme.cpp qandroidplatformtheme.h
qandroidplatformwindow.cpp qandroidplatformwindow.h
qandroidsystemlocale.cpp qandroidsystemlocale.h
androidwindowembedding.cpp androidwindowembedding.h
NO_UNITY_BUILD_SOURCES
# Conflicting symbols and macros with androidjnimain.cpp
# TODO: Unify the usage of FIND_AND_CHECK_CLASS, and similar
Expand Down
33 changes: 24 additions & 9 deletions src/plugins/platforms/android/androidjnimain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "androidjniinput.h"
#include "androidjnimain.h"
#include "androidjnimenu.h"
#include "androidwindowembedding.h"
#include "qandroidassetsfileenginehandler.h"
#include "qandroideventdispatcher.h"
#include "qandroidplatformdialoghelpers.h"
Expand Down Expand Up @@ -92,6 +93,8 @@ static const char m_methodErrorMsg[] = "Can't find method \"%s%s\"";

Q_CONSTINIT static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0);

Q_DECLARE_JNI_CLASS(QtEmbeddedDelegateFactory, "org/qtproject/qt/android/QtEmbeddedDelegateFactory")

namespace QtAndroid
{
QBasicMutex *platformInterfaceMutex()
Expand Down Expand Up @@ -187,10 +190,17 @@ namespace QtAndroid
// FIXME: avoid direct access to QtActivityDelegate
QtJniTypes::QtActivityDelegateBase qtActivityDelegate()
{
using namespace QtJniTypes;
if (!m_activityDelegate.isValid()) {
auto activity = QtAndroidPrivate::activity();
m_activityDelegate = activity.callMethod<QtJniTypes::QtActivityDelegateBase>(
"getActivityDelegate");
if (isQtApplication()) {
auto context = QtAndroidPrivate::activity();
m_activityDelegate = context.callMethod<QtActivityDelegateBase>("getActivityDelegate");
} else {
m_activityDelegate = QJniObject::callStaticMethod<QtActivityDelegateBase>(
Traits<QtEmbeddedDelegateFactory>::className(),
"getActivityDelegate",
QtAndroidPrivate::activity());
}
}

return m_activityDelegate;
Expand All @@ -213,11 +223,15 @@ namespace QtAndroid
// embedded into a native Android app, where the Activity/Service is created
// by the user, outside of Qt, and Qt content is added as a view.
JNIEnv *env = QJniEnvironment::getJniEnv();
static const jint isQtActivity = env->IsInstanceOf(QtAndroidPrivate::activity().object(),
m_qtActivityClass);
static const jint isQtService = env->IsInstanceOf(QtAndroidPrivate::service().object(),
m_qtServiceClass);
return isQtActivity || isQtService;
auto activity = QtAndroidPrivate::activity();
if (activity.isValid())
return env->IsInstanceOf(activity.object(), m_qtActivityClass);
auto service = QtAndroidPrivate::service();
if (service.isValid())
return env->IsInstanceOf(QtAndroidPrivate::service().object(), m_qtServiceClass);
// return true as default as Qt application is our default use case.
// famous last words: we should not end up here
return true;
}

void notifyAccessibilityLocationChange(uint accessibilityObjectId)
Expand Down Expand Up @@ -870,7 +884,8 @@ Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/)
|| !QtAndroidAccessibility::registerNatives(env)
|| !QtAndroidDialogHelpers::registerNatives(env)
|| !QAndroidPlatformClipboard::registerNatives(env)
|| !QAndroidPlatformWindow::registerNatives(env)) {
|| !QAndroidPlatformWindow::registerNatives(env)
|| !QtAndroidWindowEmbedding::registerNatives(env)) {
__android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed");
return -1;
}
Expand Down
Loading

0 comments on commit 702c420

Please sign in to comment.