diff options
Diffstat (limited to 'src')
10 files changed, 376 insertions, 14 deletions
diff --git a/src/android/jar/CMakeLists.txt b/src/android/jar/CMakeLists.txt index af47cd779f..698853588c 100644 --- a/src/android/jar/CMakeLists.txt +++ b/src/android/jar/CMakeLists.txt @@ -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 diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java index f8438003e1..cab542622b 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java @@ -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; @@ -22,8 +22,14 @@ import java.util.ArrayList; 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); @@ -71,6 +77,7 @@ class QtEmbeddedDelegate extends QtActivityDelegateBase implements QtNative.AppS 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(); @@ -89,6 +96,8 @@ class QtEmbeddedDelegate extends QtActivityDelegateBase implements QtNative.AppS QtDisplayManager.setApplicationDisplayMetrics(m_activity, metrics.widthPixels, metrics.heightPixels); + if (m_view != null) + createRootWindow(m_view); }); } } @@ -111,11 +120,30 @@ class QtEmbeddedDelegate extends QtActivityDelegateBase implements QtNative.AppS @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; } } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java new file mode 100644 index 0000000000..8cf89e5bc3 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java @@ -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); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java new file mode 100644 index 0000000000..6c3cbb3bfb --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java @@ -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()); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtView.java b/src/android/jar/src/org/qtproject/qt/android/QtView.java new file mode 100644 index 0000000000..a140bfa606 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtView.java @@ -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; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtWindow.java b/src/android/jar/src/org/qtproject/qt/android/QtWindow.java index da744a8d01..4f27532c0d 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtWindow.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtWindow.java @@ -140,7 +140,8 @@ public class QtWindow implements QtSurfaceInterface, QtLayout.QtTouchListener { 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)); } }); } diff --git a/src/plugins/platforms/android/CMakeLists.txt b/src/plugins/platforms/android/CMakeLists.txt index a3344bfd71..d5a275a76c 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -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 diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index bf15213b66..206bbd03d6 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -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" @@ -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() @@ -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; @@ -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) @@ -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; } diff --git a/src/plugins/platforms/android/androidwindowembedding.cpp b/src/plugins/platforms/android/androidwindowembedding.cpp new file mode 100644 index 0000000000..7d82ac6d87 --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.cpp @@ -0,0 +1,61 @@ +// Copyright (C) 2023 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 + +#include "androidwindowembedding.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtGui/qwindow.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(QtView, "org/qtproject/qt/android/QtView"); +Q_DECLARE_JNI_CLASS(QtEmbeddedDelegate, "org/qtproject/qt/android/QtEmbeddedDelegate"); + +namespace QtAndroidWindowEmbedding { + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView) + { + // QWindow should be constructed on the Qt thread rather than directly in the caller thread + // To avoid hitting checkReceiverThread assert in QCoreApplication::doNotify + QMetaObject::invokeMethod(qApp, [rootView] { + QWindow *parentWindow = QWindow::fromWinId(reinterpret_cast<WId>(rootView.object())); + rootView.callMethod<void>("createWindow", reinterpret_cast<jlong>(parentWindow)); + }); + } + + void deleteWindow(JNIEnv *, jclass, jlong windowRef) + { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + window->deleteLater(); + } + + void setWindowVisible(JNIEnv *, jclass, jlong windowRef, jboolean visible) + { + QMetaObject::invokeMethod(qApp, [windowRef, visible] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + if (visible) { + window->showNormal(); + if (!window->parent()->isVisible()) + window->parent()->show(); + } else { + window->hide(); + } + }); + } + + bool registerNatives(QJniEnvironment& env) { + using namespace QtJniTypes; + bool success = env.registerNativeMethods(Traits<QtEmbeddedDelegate>::className(), + {Q_JNI_NATIVE_SCOPED_METHOD(createRootWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(deleteWindow, QtAndroidWindowEmbedding)}); + + success &= env.registerNativeMethods(Traits<QtView>::className(), + {Q_JNI_NATIVE_SCOPED_METHOD(setWindowVisible, QtAndroidWindowEmbedding)}); + return success; + + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidwindowembedding.h b/src/plugins/platforms/android/androidwindowembedding.h new file mode 100644 index 0000000000..f9d92d4afc --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.h @@ -0,0 +1,38 @@ +// Copyright (C) 2023 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 + +#ifndef QTANDROIDWINDOWEMBEDDING_H +#define QTANDROIDWINDOWEMBEDDING_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(View, "android/view/View"); + +namespace QtAndroidWindowEmbedding +{ + bool registerNatives(QJniEnvironment& env); + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(createRootWindow) + void deleteWindow(JNIEnv *, jclass, jlong window); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(deleteWindow) + void setWindowVisible(JNIEnv *, jclass, jlong window, jboolean visible); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setWindowVisible) +}; + +QT_END_NAMESPACE + +#endif // QTANDROIDWINDOWEMBEDDING_H |