diff options
Diffstat (limited to 'src/android/jar/src')
46 files changed, 4828 insertions, 3122 deletions
diff --git a/src/android/jar/src/org/qtproject/qt/android/BackendRegister.java b/src/android/jar/src/org/qtproject/qt/android/BackendRegister.java new file mode 100644 index 0000000000..b66a593ec6 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/BackendRegister.java @@ -0,0 +1,9 @@ +// 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; + +class BackendRegister +{ + static native void registerBackend(Class interfaceType, Object interfaceObject); + static native void unregisterBackend(Class interfaceType); +} diff --git a/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java b/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java index 87257adb3d..7e601c0551 100644 --- a/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java +++ b/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java @@ -3,26 +3,26 @@ package org.qtproject.qt.android; +import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.ImageView; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.view.MotionEvent; -import android.widget.PopupWindow; -import android.app.Activity; +import android.util.DisplayMetrics; +import android.util.Log; import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; import android.view.ViewTreeObserver; +import android.widget.ImageView; +import android.widget.PopupWindow; /* This view represents one of the handle (selection or cursor handle) */ +@SuppressLint("ViewConstructor") class CursorView extends ImageView { - private CursorHandle mHandle; - // The coordinare which where clicked + private final CursorHandle mHandle; + // The coordinate which where clicked private float m_offsetX; private float m_offsetY; private boolean m_pressed = false; @@ -43,7 +43,7 @@ class CursorView extends ImageView switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { m_offsetX = ev.getRawX(); - m_offsetY = ev.getRawY() + getHeight() / 2; + m_offsetY = ev.getRawY() + (float) getHeight() / 2; m_pressed = true; break; } @@ -52,7 +52,7 @@ class CursorView extends ImageView if (!m_pressed) return false; mHandle.updatePosition(Math.round(ev.getRawX() - m_offsetX), - Math.round(ev.getRawY() - m_offsetY)); + Math.round(ev.getRawY() - m_offsetY)); break; } @@ -63,24 +63,24 @@ class CursorView extends ImageView } return true; } - } // Helper class that manages a cursor or selection handle -public class CursorHandle implements ViewTreeObserver.OnPreDrawListener +class CursorHandle implements ViewTreeObserver.OnPreDrawListener { - private View m_layout = null; + private static final String QtTag = "QtCursorHandle"; + private final View m_layout; private CursorView m_cursorView = null; private PopupWindow m_popup = null; - private int m_id; - private int m_attr; - private Activity m_activity; + private final int m_id; + private final int m_attr; + private final Activity m_activity; private int m_posX = 0; private int m_posY = 0; private int m_lastX; private int m_lastY; int tolerance; - private boolean m_rtl; + private final boolean m_rtl; int m_yShift; public CursorHandle(Activity activity, View layout, int id, int attr, boolean rtl) { @@ -95,27 +95,31 @@ public class CursorHandle implements ViewTreeObserver.OnPreDrawListener m_rtl = rtl; } - private boolean initOverlay(){ - if (m_popup == null){ + private void initOverlay(){ + if (m_popup != null) + return; - Context context = m_layout.getContext(); - int[] attrs = {m_attr}; - TypedArray a = context.getTheme().obtainStyledAttributes(attrs); - Drawable drawable = a.getDrawable(0); + Context context = m_layout.getContext(); + int[] attrs = {m_attr}; + TypedArray a = context.getTheme().obtainStyledAttributes(attrs); + Drawable drawable = a.getDrawable(0); - m_cursorView = new CursorView(context, this); - m_cursorView.setImageDrawable(drawable); + m_cursorView = new CursorView(context, this); + m_cursorView.setImageDrawable(drawable); - m_popup = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); - m_popup.setSplitTouchEnabled(true); - m_popup.setClippingEnabled(false); - m_popup.setContentView(m_cursorView); + m_popup = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); + m_popup.setSplitTouchEnabled(true); + m_popup.setClippingEnabled(false); + m_popup.setContentView(m_cursorView); + if (drawable != null) { m_popup.setWidth(drawable.getIntrinsicWidth()); m_popup.setHeight(drawable.getIntrinsicHeight()); - - m_layout.getViewTreeObserver().addOnPreDrawListener(this); + } else { + Log.w(QtTag, "initOverlay(): cannot get width/height for popup " + + "from null drawable for attribute " + m_attr); } - return true; + + m_layout.getViewTreeObserver().addOnPreDrawListener(this); } // Show the handle at a given position (or move it if it is already shown) @@ -134,9 +138,9 @@ public class CursorHandle implements ViewTreeObserver.OnPreDrawListener int x2 = x + layoutLocation[0] - activityLocation[0]; int y2 = y + layoutLocation[1] + m_yShift + (activityLocationInWindow[1] - activityLocation[1]); - if (m_id == QtNative.IdCursorHandle) { + if (m_id == QtInputDelegate.IdCursorHandle) { x2 -= m_popup.getWidth() / 2 ; - } else if ((m_id == QtNative.IdLeftHandle && !m_rtl) || (m_id == QtNative.IdRightHandle && m_rtl)) { + } else if ((m_id == QtInputDelegate.IdLeftHandle && !m_rtl) || (m_id == QtInputDelegate.IdRightHandle && m_rtl)) { x2 -= m_popup.getWidth() * 3 / 4; } else { x2 -= m_popup.getWidth() / 4; @@ -176,7 +180,7 @@ public class CursorHandle implements ViewTreeObserver.OnPreDrawListener public void updatePosition(int x, int y) { y -= m_yShift; if (Math.abs(m_lastX - x) > tolerance || Math.abs(m_lastY - y) > tolerance) { - QtNative.handleLocationChanged(m_id, x + m_posX, y + m_posY); + QtInputDelegate.handleLocationChanged(m_id, x + m_posX, y + m_posY); m_lastX = x; m_lastY = y; } diff --git a/src/android/jar/src/org/qtproject/qt/android/EditContextView.java b/src/android/jar/src/org/qtproject/qt/android/EditContextView.java index 54b6c14137..fbd32ed98b 100644 --- a/src/android/jar/src/org/qtproject/qt/android/EditContextView.java +++ b/src/android/jar/src/org/qtproject/qt/android/EditContextView.java @@ -4,25 +4,27 @@ package org.qtproject.qt.android; +import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Point; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; -import android.R; import java.util.HashMap; -public class EditContextView extends LinearLayout implements View.OnClickListener +@SuppressLint("ViewConstructor") +class EditContextView extends LinearLayout implements View.OnClickListener { - public static final int CUT_BUTTON = 1 << 0; + public static final int CUT_BUTTON = 1; public static final int COPY_BUTTON = 1 << 1; public static final int PASTE_BUTTON = 1 << 2; - public static final int SALL_BUTTON = 1 << 3; + public static final int SELECT_ALL_BUTTON = 1 << 3; - HashMap<Integer, ContextButton> m_buttons = new HashMap<Integer, ContextButton>(4); + HashMap<Integer, ContextButton> m_buttons = new HashMap<>(4); OnClickListener m_onClickListener; public interface OnClickListener @@ -40,8 +42,10 @@ public class EditContextView extends LinearLayout implements View.OnClickListene setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); setGravity(Gravity.CENTER); - setTextColor(getResources().getColor(R.color.widget_edittext_dark)); - EditContextView.this.setBackground(getResources().getDrawable(R.drawable.editbox_background_normal)); + setTextColor(getResources().getColor( + android.R.color.widget_edittext_dark, context.getTheme())); + EditContextView.this.setBackground(getResources().getDrawable( + android.R.drawable.editbox_background_normal, context.getTheme())); float scale = getResources().getDisplayMetrics().density; int hPadding = (int)(16 * scale + 0.5f); int vPadding = (int)(8 * scale + 0.5f); @@ -68,10 +72,38 @@ public class EditContextView extends LinearLayout implements View.OnClickListene public void updateButtons(int buttonsLayout) { - m_buttons.get(R.string.cut).setVisibility((buttonsLayout & CUT_BUTTON) != 0 ? View.VISIBLE : View.GONE); - m_buttons.get(R.string.copy).setVisibility((buttonsLayout & COPY_BUTTON) != 0 ? View.VISIBLE : View.GONE); - m_buttons.get(R.string.paste).setVisibility((buttonsLayout & PASTE_BUTTON) != 0 ? View.VISIBLE : View.GONE); - m_buttons.get(R.string.selectAll).setVisibility((buttonsLayout & SALL_BUTTON) != 0 ? View.VISIBLE : View.GONE); + ContextButton button = m_buttons.get(android.R.string.cut); + if (button != null) + button.setVisibility((buttonsLayout & CUT_BUTTON) != 0 ? View.VISIBLE : View.GONE); + + button = m_buttons.get(android.R.string.copy); + if (button != null) + button.setVisibility((buttonsLayout & COPY_BUTTON) != 0 ? View.VISIBLE : View.GONE); + + button = m_buttons.get(android.R.string.paste); + if (button != null) + button.setVisibility((buttonsLayout & PASTE_BUTTON) != 0 ? View.VISIBLE : View.GONE); + + button = m_buttons.get(android.R.string.selectAll); + if (button != null) + button.setVisibility((buttonsLayout & SELECT_ALL_BUTTON) != 0 ? View.VISIBLE : View.GONE); + } + + public Point getCalculatedSize() + { + Point size = new Point(0, 0); + for (ContextButton b : m_buttons.values()) { + if (b.getVisibility() == View.VISIBLE) { + b.measure(0, 0); + size.x += b.getMeasuredWidth(); + size.y = Math.max(size.y, b.getMeasuredHeight()); + } + } + + size.x += getPaddingLeft() + getPaddingRight(); + size.y += getPaddingTop() + getPaddingBottom(); + + return size; } public EditContextView(Context context, OnClickListener onClickListener) { @@ -79,9 +111,9 @@ public class EditContextView extends LinearLayout implements View.OnClickListene m_onClickListener = onClickListener; setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - addButton(R.string.cut); - addButton(R.string.copy); - addButton(R.string.paste); - addButton(R.string.selectAll); + addButton(android.R.string.cut); + addButton(android.R.string.copy); + addButton(android.R.string.paste); + addButton(android.R.string.selectAll); } } diff --git a/src/android/jar/src/org/qtproject/qt/android/EditPopupMenu.java b/src/android/jar/src/org/qtproject/qt/android/EditPopupMenu.java index 00cbb97561..25be522c48 100644 --- a/src/android/jar/src/org/qtproject/qt/android/EditPopupMenu.java +++ b/src/android/jar/src/org/qtproject/qt/android/EditPopupMenu.java @@ -4,32 +4,22 @@ package org.qtproject.qt.android; +import android.app.Activity; import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; +import android.graphics.Point; import android.view.View; -import android.widget.LinearLayout; -import android.widget.ImageView; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.view.MotionEvent; -import android.widget.PopupWindow; -import android.app.Activity; -import android.view.ViewTreeObserver; -import android.view.View.OnClickListener; -import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup; -import android.R; +import android.view.ViewTreeObserver; +import android.widget.PopupWindow; // Helper class that manages a cursor or selection handle -public class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.OnLayoutChangeListener, +class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.OnLayoutChangeListener, EditContextView.OnClickListener { - private View m_layout = null; - private EditContextView m_view = null; + private final View m_layout; + private final EditContextView m_view; private PopupWindow m_popup = null; - private Activity m_activity; + private final Activity m_activity; private int m_posX; private int m_posY; private int m_buttons; @@ -69,6 +59,8 @@ public class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.O initOverlay(); m_view.updateButtons(buttons); + Point viewSize = m_view.getCalculatedSize(); + final int[] layoutLocation = new int[2]; m_layout.getLocationOnScreen(layoutLocation); @@ -81,9 +73,9 @@ public class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.O int x2 = x + layoutLocation[0] - activityLocation[0]; int y2 = y + layoutLocation[1] + (activityLocationInWindow[1] - activityLocation[1]); - x2 -= m_view.getWidth() / 2 ; + x2 -= viewSize.x / 2 ; - y2 -= m_view.getHeight(); + y2 -= viewSize.y; if (y2 < 0) { if (cursorHandle != null) { y2 = cursorHandle.bottom(); @@ -94,8 +86,8 @@ public class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.O } } - if (m_layout.getWidth() < x + m_view.getWidth() / 2) - x2 = m_layout.getWidth() - m_view.getWidth(); + if (m_layout.getWidth() < x + viewSize.x / 2) + x2 = m_layout.getWidth() - viewSize.x; if (x2 < 0) x2 = 0; @@ -143,16 +135,16 @@ public class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.O @Override public void contextButtonClicked(int buttonId) { switch (buttonId) { - case R.string.cut: + case android.R.string.cut: QtNativeInputConnection.cut(); break; - case R.string.copy: + case android.R.string.copy: QtNativeInputConnection.copy(); break; - case R.string.paste: + case android.R.string.paste: QtNativeInputConnection.paste(); break; - case R.string.selectAll: + case android.R.string.selectAll: QtNativeInputConnection.selectAll(); break; } diff --git a/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java index 672a2e28d3..6780634317 100644 --- a/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java +++ b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java @@ -5,8 +5,10 @@ package org.qtproject.qt.android; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -34,7 +36,6 @@ import android.graphics.drawable.ScaleDrawable; import android.graphics.drawable.StateListDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Build; -import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -60,7 +61,7 @@ import java.util.Map; import java.util.Objects; -public class ExtractStyle { +class ExtractStyle { // This used to be retrieved from android.R.styleable.ViewDrawableStates field via reflection, // but since the access to that is restricted, we need to have hard-coded here. @@ -136,26 +137,56 @@ public class ExtractStyle { Context m_context; private final HashMap<String, DrawableCache> m_drawableCache = new HashMap<>(); - private static final String EXTRACT_STYLE_KEY = "extract.android.style"; - private static final String EXTRACT_STYLE_MINIMAL_KEY = "extract.android.style.option"; - private static boolean m_missingNormalStyle = false; private static boolean m_missingDarkStyle = false; private static String m_stylePath = null; private static boolean m_extractMinimal = false; - public static void setup(Bundle loaderParams) { - if (loaderParams.containsKey(EXTRACT_STYLE_KEY)) { - m_stylePath = loaderParams.getString(EXTRACT_STYLE_KEY); + private static final String QtTAG = "QtExtractStyle"; + + private static boolean isUiModeDark(Configuration config) + { + return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + public static String setup(Context context, String extractOption, int dpi) { + + String dataDir = context.getApplicationInfo().dataDir; + m_stylePath = dataDir + "/qt-reserved-files/android-style/" + dpi + "/"; + + if (extractOption.equals("none")) + return m_stylePath; - boolean darkModeFileMissing = !(new File(m_stylePath + "darkUiMode/style.json").exists()); - m_missingDarkStyle = Build.VERSION.SDK_INT > 28 && darkModeFileMissing; + if (extractOption.isEmpty()) + extractOption = "minimal"; - m_missingNormalStyle = !(new File(m_stylePath + "style.json").exists()); + if (!extractOption.equals("default") && !extractOption.equals("full") + && !extractOption.equals("minimal") && !extractOption.equals("none")) { + Log.e(QtTAG, "Invalid extract_android_style option \"" + extractOption + + "\", defaulting to \"minimal\""); + extractOption = "minimal"; + } - m_extractMinimal = loaderParams.containsKey(EXTRACT_STYLE_MINIMAL_KEY) && - loaderParams.getBoolean(EXTRACT_STYLE_MINIMAL_KEY); + // QTBUG-69810: The extraction code will trigger compatibility warnings on Android + // SDK version >= 28 when the target SDK version is set to something lower then 28, + // so default to "none" and issue a warning if that is the case. + if (extractOption.equals("default")) { + int targetSdk = context.getApplicationInfo().targetSdkVersion; + if (targetSdk < 28 && Build.VERSION.SDK_INT >= 28) { + Log.e(QtTAG, "extract_android_style option set to \"none\" when " + + "targetSdkVersion is less then 28"); + extractOption = "none"; + } } + + boolean darkModeFileMissing = !(new File(m_stylePath + "darkUiMode/style.json").exists()); + m_missingDarkStyle = Build.VERSION.SDK_INT > 28 && darkModeFileMissing; + m_missingNormalStyle = !(new File(m_stylePath + "style.json").exists()); + m_extractMinimal = extractOption.equals("minimal"); + + ExtractStyle.runIfNeeded(context, isUiModeDark(context.getResources().getConfiguration())); + + return m_stylePath; } public static void runIfNeeded(Context context, boolean extractDarkMode) { @@ -1104,10 +1135,6 @@ public class ExtractStyle { return json; } - public JSONObject extractTextAppearanceInformation(int styleName, String qtClass, AttributeSet attributeSet) { - return extractTextAppearanceInformation(styleName, qtClass, android.R.attr.textAppearance, attributeSet); - } - public JSONObject extractTextAppearanceInformation(int styleName, String qtClass) { return extractTextAppearanceInformation(styleName, qtClass, android.R.attr.textAppearance, null); } diff --git a/src/android/jar/src/org/qtproject/qt/android/accessibility/QtAccessibilityDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityDelegate.java index 74d5ce5dde..8558e42c3b 100644 --- a/src/android/jar/src/org/qtproject/qt/android/accessibility/QtAccessibilityDelegate.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityDelegate.java @@ -2,37 +2,31 @@ // 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.accessibility; +package org.qtproject.qt.android; -import android.accessibilityservice.AccessibilityService; import android.app.Activity; +import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.system.Os; +import android.text.TextUtils; import android.util.Log; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import android.text.TextUtils; - -import android.view.accessibility.*; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; -import android.view.MotionEvent; -import android.view.View.OnHoverListener; +import android.view.accessibility.AccessibilityNodeProvider; -import android.content.Context; -import android.system.Os; - -import java.util.LinkedList; -import java.util.List; - -import org.qtproject.qt.android.QtActivityDelegate; - -public class QtAccessibilityDelegate extends View.AccessibilityDelegate +class QtAccessibilityDelegate extends View.AccessibilityDelegate { private static final String TAG = "Qt A11Y"; - // Qt uses the upper half of the unsiged integers + // Qt uses the upper half of the unsigned integers // all low positive ints should be fine. public static final int INVALID_ID = 333; // half evil @@ -41,10 +35,8 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate private static final String DEFAULT_CLASS_NAME = "$VirtualChild"; private View m_view = null; - private AccessibilityManager m_manager; - private QtActivityDelegate m_activityDelegate; - private Activity m_activity; - private ViewGroup m_layout; + private final AccessibilityManager m_manager; + private final QtLayout m_layout; // The accessible object that currently has the "accessibility focus" // usually indicated by a yellow rectangle on screen. @@ -67,14 +59,15 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate return dispatchHoverEvent(event); } } - - public QtAccessibilityDelegate(Activity activity, ViewGroup layout, QtActivityDelegate activityDelegate) + // TODO do we want to have one QtAccessibilityDelegate for the whole app (QtRootLayout) or + // e.g. one per window? + // FIXME make QtAccessibilityDelegate window based or verify current way works + // also for child windows: QTBUG-120685 + public QtAccessibilityDelegate(QtLayout layout) { - m_activity = activity; m_layout = layout; - m_activityDelegate = activityDelegate; - m_manager = (AccessibilityManager) m_activity.getSystemService(Context.ACCESSIBILITY_SERVICE); + m_manager = (AccessibilityManager) m_layout.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (m_manager != null) { AccessibilityManagerListener accServiceListener = new AccessibilityManagerListener(); if (!m_manager.addAccessibilityStateChangeListener(accServiceListener)) @@ -92,23 +85,26 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate if (Os.getenv("QT_ANDROID_DISABLE_ACCESSIBILITY") != null) return; if (enabled) { - try { + try { View view = m_view; if (view == null) { - view = new View(m_activity); + view = new View(m_layout.getContext()); view.setId(View.NO_ID); } // ### Keep this for debugging for a while. It allows us to visually see that our View // ### is on top of the surface(s) - // ColorDrawable color = new ColorDrawable(0x80ff8080); //0xAARRGGBB - // view.setBackground(color); + //noinspection CommentedOutCode + { + // ColorDrawable color = new ColorDrawable(0x80ff8080); //0xAARRGGBB + // view.setBackground(color); + } view.setAccessibilityDelegate(QtAccessibilityDelegate.this); // if all is fine, add it to the layout if (m_view == null) { //m_layout.addAccessibilityView(view); - m_layout.addView(view, m_activityDelegate.getSurfaceCount(), + m_layout.addView(view, m_layout.getChildCount(), new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } m_view = view; @@ -116,7 +112,7 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate m_view.setOnHoverListener(new HoverEventListener()); } catch (Exception e) { // Unknown exception means something went wrong. - Log.w("Qt A11y", "Unknown exception: " + e.toString()); + Log.w("Qt A11y", "Unknown exception: " + e); } } else { if (m_view != null) { @@ -152,8 +148,6 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate switch (event.getAction()) { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: - setHoveredVirtualViewId(virtualViewId); - break; case MotionEvent.ACTION_HOVER_EXIT: setHoveredVirtualViewId(virtualViewId); break; @@ -164,95 +158,113 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate public void notifyScrolledEvent(int viewId) { - sendEventForVirtualViewId(viewId, AccessibilityEvent.TYPE_VIEW_SCROLLED); + QtNative.runAction(() -> sendEventForVirtualViewId(viewId, + AccessibilityEvent.TYPE_VIEW_SCROLLED)); } public void notifyLocationChange(int viewId) { - if (m_focusedVirtualViewId == viewId) - invalidateVirtualViewId(m_focusedVirtualViewId); + QtNative.runAction(() -> { + if (m_focusedVirtualViewId == viewId) + invalidateVirtualViewId(m_focusedVirtualViewId); + }); } public void notifyObjectHide(int viewId, int parentId) { - // If the object had accessibility focus, we need to clear it. - // Note: This code is mostly copied from - // AccessibilityNodeProvider::performAction, but we remove the - // focus only if the focused view id matches the one that was hidden. - if (m_focusedVirtualViewId == viewId) { - m_focusedVirtualViewId = INVALID_ID; - m_view.invalidate(); - sendEventForVirtualViewId(viewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - } - // When the object is hidden, we need to notify its parent about - // content change, not the hidden object itself - invalidateVirtualViewId(parentId); + QtNative.runAction(() -> { + // If the object had accessibility focus, we need to clear it. + // Note: This code is mostly copied from + // AccessibilityNodeProvider::performAction, but we remove the + // focus only if the focused view id matches the one that was hidden. + if (m_focusedVirtualViewId == viewId) { + m_focusedVirtualViewId = INVALID_ID; + m_view.invalidate(); + sendEventForVirtualViewId(viewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + } + // When the object is hidden, we need to notify its parent about + // content change, not the hidden object itself + invalidateVirtualViewId(parentId); + }); + } + + public void notifyObjectShow(int parentId) + { + QtNative.runAction(() -> { + // When the object is shown, we need to notify its parent about + // content change, not the shown object itself + invalidateVirtualViewId(parentId); + }); } public void notifyObjectFocus(int viewId) { - if (m_view == null) - return; - m_focusedVirtualViewId = viewId; - m_view.invalidate(); - sendEventForVirtualViewId(viewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + QtNative.runAction(() -> { + if (m_view == null) + return; + m_focusedVirtualViewId = viewId; + m_view.invalidate(); + sendEventForVirtualViewId(viewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + }); } public void notifyValueChanged(int viewId, String value) { - // Send a TYPE_ANNOUNCEMENT event with the new value + QtNative.runAction(() -> { + // Send a TYPE_ANNOUNCEMENT event with the new value - if ((viewId == INVALID_ID) || !m_manager.isEnabled()) { - Log.w(TAG, "notifyValueChanged() for invalid view"); - return; - } + if ((viewId == INVALID_ID) || !m_manager.isEnabled()) { + Log.w(TAG, "notifyValueChanged() for invalid view"); + return; + } - final ViewGroup group = (ViewGroup)m_view.getParent(); - if (group == null) { - Log.w(TAG, "Could not announce value because ViewGroup was null."); - return; - } + final ViewGroup group = (ViewGroup) m_view.getParent(); + if (group == null) { + Log.w(TAG, "Could not announce value because ViewGroup was null."); + return; + } - final AccessibilityEvent event = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); + final AccessibilityEvent event = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); - event.setEnabled(true); - event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); + event.setEnabled(true); + event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); - event.setContentDescription(value); + event.setContentDescription(value); - if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) { - Log.w(TAG, "No value to announce for " + event.getClassName()); - return; - } + if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) { + Log.w(TAG, "No value to announce for " + event.getClassName()); + return; + } - event.setPackageName(m_view.getContext().getPackageName()); - event.setSource(m_view, viewId); + event.setPackageName(m_view.getContext().getPackageName()); + event.setSource(m_view, viewId); - if (!group.requestSendAccessibilityEvent(m_view, event)) - Log.w(TAG, "Failed to send value change announcement for " + event.getClassName()); + if (!group.requestSendAccessibilityEvent(m_view, event)) + Log.w(TAG, "Failed to send value change announcement for " + event.getClassName()); + }); } - public boolean sendEventForVirtualViewId(int virtualViewId, int eventType) + public void sendEventForVirtualViewId(int virtualViewId, int eventType) { final AccessibilityEvent event = getEventForVirtualViewId(virtualViewId, eventType); - return sendAccessibilityEvent(event); + sendAccessibilityEvent(event); } - public boolean sendAccessibilityEvent(AccessibilityEvent event) + public void sendAccessibilityEvent(AccessibilityEvent event) { if (event == null) - return false; + return; final ViewGroup group = (ViewGroup) m_view.getParent(); if (group == null) { Log.w(TAG, "Could not send AccessibilityEvent because group was null. This should really not happen."); - return false; + return; } - return group.requestSendAccessibilityEvent(m_view, event); + group.requestSendAccessibilityEvent(m_view, event); } public void invalidateVirtualViewId(int virtualViewId) @@ -285,7 +297,7 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate return null; } - if (m_activityDelegate.getSurfaceCount() == 0) + if (m_layout.getChildCount() == 0) return null; final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); @@ -302,15 +314,17 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate return event; } + // This can be used for debug by performActionForVirtualViewId() + /** @noinspection unused*/ private void dumpNodes(int parentId) { Log.i(TAG, "A11Y hierarchy: " + parentId + " parent: " + QtNativeAccessibility.parentId(parentId)); Log.i(TAG, " desc: " + QtNativeAccessibility.descriptionForAccessibleObject(parentId) + " rect: " + QtNativeAccessibility.screenRect(parentId)); Log.i(TAG, " NODE: " + getNodeForVirtualViewId(parentId)); int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(parentId); - for (int i = 0; i < ids.length; ++i) { - Log.i(TAG, parentId + " has child: " + ids[i]); - dumpNodes(ids[i]); + for (int id : ids) { + Log.i(TAG, parentId + " has child: " + id); + dumpNodes(id); } } @@ -347,13 +361,13 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate result.setPackageName(source.getPackageName()); result.setClassName(source.getClassName()); -// Spit out the entire hierarchy for debugging purposes -// dumpNodes(-1); + // Spit out the entire hierarchy for debugging purposes + // dumpNodes(-1); - if (m_activityDelegate.getSurfaceCount() != 0) { + if (m_layout.getChildCount() != 0) { int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(-1); - for (int i = 0; i < ids.length; ++i) - result.addChild(m_view, ids[i]); + for (int id : ids) + result.addChild(m_view, id); } // The offset values have changed, so we need to re-focus the @@ -382,7 +396,7 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate node.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); node.setPackageName(m_view.getContext().getPackageName()); - if (m_activityDelegate.getSurfaceCount() == 0 || !QtNativeAccessibility.populateNode(virtualViewId, node)) { + if (m_layout.getChildCount() == 0 || !QtNativeAccessibility.populateNode(virtualViewId, node)) { return node; } @@ -401,23 +415,22 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate screenRect.offset(offsetX, offsetY); node.setBoundsInScreen(screenRect); - Rect rectInParent = screenRect; Rect parentScreenRect = QtNativeAccessibility.screenRect(parentId); - rectInParent.offset(-parentScreenRect.left, -parentScreenRect.top); - node.setBoundsInParent(rectInParent); + screenRect.offset(-parentScreenRect.left, -parentScreenRect.top); + node.setBoundsInParent(screenRect); // Manage internal accessibility focus state. if (m_focusedVirtualViewId == virtualViewId) { node.setAccessibilityFocused(true); - node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } else { node.setAccessibilityFocused(false); - node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); } int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(virtualViewId); - for (int i = 0; i < ids.length; ++i) - node.addChild(m_view, ids[i]); + for (int id : ids) + node.addChild(m_view, id); if (node.isScrollable()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { node.setCollectionInfo(new CollectionInfo(ids.length, 1, false)); @@ -429,12 +442,12 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate return node; } - private AccessibilityNodeProvider m_nodeProvider = new AccessibilityNodeProvider() + private final AccessibilityNodeProvider m_nodeProvider = new AccessibilityNodeProvider() { @Override public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { - if (virtualViewId == View.NO_ID || m_activityDelegate.getSurfaceCount() == 0) { + if (virtualViewId == View.NO_ID || m_layout.getChildCount() == 0) { return getNodeForView(); } return getNodeForVirtualViewId(virtualViewId); @@ -476,16 +489,19 @@ public class QtAccessibilityDelegate extends View.AccessibilityDelegate return m_view.performAccessibilityAction(action, arguments); } } - handled |= performActionForVirtualViewId(virtualViewId, action, arguments); + handled |= performActionForVirtualViewId(virtualViewId, action); return handled; } }; - protected boolean performActionForVirtualViewId(int virtualViewId, int action, Bundle arguments) + protected boolean performActionForVirtualViewId(int virtualViewId, int action) { -// Log.i(TAG, "ACTION " + action + " on " + virtualViewId); -// dumpNodes(virtualViewId); + //noinspection CommentedOutCode + { + // Log.i(TAG, "ACTION " + action + " on " + virtualViewId); + // dumpNodes(virtualViewId); + } boolean success = false; switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: diff --git a/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityInterface.java new file mode 100644 index 0000000000..690b1ae248 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityInterface.java @@ -0,0 +1,14 @@ +// 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; + +@UsedFromNativeCode +interface QtAccessibilityInterface { + default void initializeAccessibility() { } + default void notifyLocationChange(int viewId) { } + default void notifyObjectHide(int viewId, int parentId) { } + default void notifyObjectFocus(int viewId) { } + default void notifyScrolledEvent(int viewId) { } + default void notifyValueChanged(int viewId, String value) { } + default void notifyObjectShow(int parentId) { } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java new file mode 100644 index 0000000000..b5de60d49d --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java @@ -0,0 +1,337 @@ +// 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 + +package org.qtproject.qt.android; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Browser; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; + +public class QtActivityBase extends Activity implements QtNative.AppStateDetailsListener +{ + private String m_applicationParams = ""; + private boolean m_isCustomThemeSet = false; + private boolean m_retainNonConfigurationInstance = false; + + private QtActivityDelegate m_delegate; + + public static final String EXTRA_SOURCE_INFO = "org.qtproject.qt.android.sourceInfo"; + + private void addReferrer(Intent intent) + { + if (intent.getExtras() != null && intent.getExtras().getString(EXTRA_SOURCE_INFO) != null) + return; + + String browserApplicationId = ""; + if (intent.getExtras() != null) + browserApplicationId = intent.getExtras().getString(Browser.EXTRA_APPLICATION_ID); + + String sourceInformation = ""; + if (browserApplicationId != null && !browserApplicationId.isEmpty()) { + sourceInformation = browserApplicationId; + } else { + Uri referrer = getReferrer(); + if (referrer != null) + sourceInformation = referrer.toString().replaceFirst("android-app://", ""); + } + + intent.putExtra(EXTRA_SOURCE_INFO, sourceInformation); + } + + // Append any parameters to your application. + // Either a whitespace or a tab is accepted as a separator between parameters. + /** @noinspection unused*/ + public void appendApplicationParameters(String params) + { + if (params == null || params.isEmpty()) + return; + + if (!m_applicationParams.isEmpty()) + m_applicationParams += " "; + m_applicationParams += params; + } + + private void handleActivityRestart() { + if (QtNative.getStateDetails().isStarted) { + boolean updated = m_delegate.updateActivityAfterRestart(this); + if (!updated) { + // could not update the activity so restart the application + Intent intent = Intent.makeRestartActivityTask(getComponentName()); + startActivity(intent); + QtNative.quitApp(); + Runtime.getRuntime().exit(0); + } + } + } + + @Override + public void setTheme(int resId) { + super.setTheme(resId); + m_isCustomThemeSet = true; + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + + if (!m_isCustomThemeSet) { + setTheme(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + android.R.style.Theme_DeviceDefault_DayNight : + android.R.style.Theme_Holo_Light); + } + + m_delegate = new QtActivityDelegate(this); + + QtNative.registerAppStateListener(this); + + handleActivityRestart(); + addReferrer(getIntent()); + + QtActivityLoader loader = new QtActivityLoader(this); + loader.appendApplicationParameters(m_applicationParams); + + loader.loadQtLibraries(); + m_delegate.startNativeApplication(loader.getApplicationParameters(), + loader.getMainLibraryPath()); + } + + @Override + public void onAppStateDetailsChanged(QtNative.ApplicationStateDetails details) { + if (details.isStarted) + m_delegate.registerBackends(); + else + m_delegate.unregisterBackends(); + } + + @Override + protected void onStart() + { + super.onStart(); + } + + @Override + protected void onRestart() + { + super.onRestart(); + } + + @Override + protected void onPause() + { + super.onPause(); + if (Build.VERSION.SDK_INT < 24 || !isInMultiWindowMode()) + QtNative.setApplicationState(QtNative.ApplicationState.ApplicationInactive); + m_delegate.displayManager().unregisterDisplayListener(); + } + + @Override + protected void onResume() + { + super.onResume(); + QtNative.setApplicationState(QtNative.ApplicationState.ApplicationActive); + if (QtNative.getStateDetails().isStarted) { + m_delegate.displayManager().registerDisplayListener(); + QtNative.updateWindow(); + // Suspending the app clears the immersive mode, so we need to set it again. + m_delegate.displayManager().updateFullScreen(); + } + } + + @Override + protected void onStop() + { + super.onStop(); + QtNative.setApplicationState(QtNative.ApplicationState.ApplicationSuspended); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + if (!m_retainNonConfigurationInstance) { + QtNative.unregisterAppStateListener(this); + QtNative.terminateQt(); + QtNative.setActivity(null); + QtNative.getQtThread().exit(); + System.exit(0); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + m_delegate.handleUiModeChange(newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK); + } + + @Override + public boolean onContextItemSelected(MenuItem item) + { + m_delegate.setContextMenuVisible(false); + return QtNative.onContextItemSelected(item.getItemId(), item.isChecked()); + } + + @Override + public void onContextMenuClosed(Menu menu) + { + if (!m_delegate.isContextMenuVisible()) + return; + m_delegate.setContextMenuVisible(false); + QtNative.onContextMenuClosed(menu); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) + { + menu.clearHeader(); + QtNative.onCreateContextMenu(menu); + m_delegate.setContextMenuVisible(true); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) + { + boolean handleResult = m_delegate.getInputDelegate().handleDispatchKeyEvent(event); + if (QtNative.getStateDetails().isStarted && handleResult) + return true; + + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) + { + boolean handled = m_delegate.getInputDelegate().handleDispatchGenericMotionEvent(event); + if (QtNative.getStateDetails().isStarted && handled) + return true; + + return super.dispatchGenericMotionEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) + { + QtNative.ApplicationStateDetails stateDetails = QtNative.getStateDetails(); + if (!stateDetails.isStarted || !stateDetails.nativePluginIntegrationReady) + return false; + + return m_delegate.getInputDelegate().onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) + { + QtNative.ApplicationStateDetails stateDetails = QtNative.getStateDetails(); + if (!stateDetails.isStarted || !stateDetails.nativePluginIntegrationReady) + return false; + + return m_delegate.getInputDelegate().onKeyUp(keyCode, event); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + menu.clear(); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) + { + boolean res = QtNative.onPrepareOptionsMenu(menu); + m_delegate.setActionBarVisibility(res && menu.size() > 0); + return res; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + return QtNative.onOptionsItemSelected(item.getItemId(), item.isChecked()); + } + + @Override + public void onOptionsMenuClosed(Menu menu) + { + QtNative.onOptionsMenuClosed(menu); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) + { + super.onRestoreInstanceState(savedInstanceState); + QtNative.setStarted(savedInstanceState.getBoolean("Started")); + int savedSystemUiVisibility = savedInstanceState.getInt("SystemUiVisibility"); + m_delegate.displayManager().setSystemUiVisibility(savedSystemUiVisibility); + // FIXME restore all surfaces + } + + @Override + public Object onRetainNonConfigurationInstance() + { + super.onRetainNonConfigurationInstance(); + m_retainNonConfigurationInstance = true; + return true; + } + + @Override + protected void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putInt("SystemUiVisibility", m_delegate.displayManager().systemUiVisibility()); + outState.putBoolean("Started", QtNative.getStateDetails().isStarted); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) + { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) + m_delegate.displayManager().updateFullScreen(); + } + + @Override + protected void onNewIntent(Intent intent) + { + addReferrer(intent); + QtNative.onNewIntent(intent); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + QtNative.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) + { + QtNative.sendRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @UsedFromNativeCode + public void hideSplashScreen(final int duration) + { + m_delegate.hideSplashScreen(duration); + } + + @UsedFromNativeCode + QtActivityDelegateBase getActivityDelegate() + { + return m_delegate; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java index 31440cfa02..596074c631 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java @@ -1,827 +1,180 @@ // Copyright (C) 2017 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com> // 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.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.AssetManager; import android.content.res.Configuration; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.Rect; -import android.net.LocalServerSocket; -import android.net.LocalSocket; import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; -import android.text.method.MetaKeyKeyListener; -import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import android.view.Display; +import android.view.ViewTreeObserver; import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.Display; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.Surface; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowInsetsController; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; -import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.PopupMenu; -import android.hardware.display.DisplayManager; -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileWriter; -import java.io.InputStreamReader; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.HashMap; -import java.util.Objects; -import org.qtproject.qt.android.accessibility.QtAccessibilityDelegate; - -public class QtActivityDelegate +class QtActivityDelegate extends QtActivityDelegateBase + implements QtWindowInterface, QtAccessibilityInterface, QtMenuInterface, QtLayoutInterface { - private Activity m_activity = null; - private Method m_super_dispatchKeyEvent = null; - private Method m_super_onRestoreInstanceState = null; - private Method m_super_onRetainNonConfigurationInstance = null; - private Method m_super_onSaveInstanceState = null; - private Method m_super_onKeyDown = null; - private Method m_super_onKeyUp = null; - private Method m_super_onConfigurationChanged = null; - private Method m_super_onActivityResult = null; - private Method m_super_dispatchGenericMotionEvent = null; - private Method m_super_onWindowFocusChanged = null; - - private static final String NATIVE_LIBRARIES_KEY = "native.libraries"; - private static final String BUNDLED_LIBRARIES_KEY = "bundled.libraries"; - private static final String MAIN_LIBRARY_KEY = "main.library"; - private static final String ENVIRONMENT_VARIABLES_KEY = "environment.variables"; - private static final String APPLICATION_PARAMETERS_KEY = "application.parameters"; - private static final String STATIC_INIT_CLASSES_KEY = "static.init.classes"; - - public static final int SYSTEM_UI_VISIBILITY_NORMAL = 0; - public static final int SYSTEM_UI_VISIBILITY_FULLSCREEN = 1; - public static final int SYSTEM_UI_VISIBILITY_TRANSLUCENT = 2; + private static final String QtTAG = "QtActivityDelegate"; - private static String m_applicationParameters = null; - - private int m_currentRotation = -1; // undefined - private int m_nativeOrientation = Configuration.ORIENTATION_UNDEFINED; - - private String m_mainLib; - private long m_metaState; - private int m_lastChar = 0; - private int m_softInputMode = 0; - private int m_systemUiVisibility = SYSTEM_UI_VISIBILITY_NORMAL; - private boolean m_started = false; - private HashMap<Integer, QtSurface> m_surfaces = null; - private HashMap<Integer, View> m_nativeViews = null; - private QtLayout m_layout = null; + private QtRootLayout m_layout = null; private ImageView m_splashScreen = null; private boolean m_splashScreenSticky = false; - private QtEditText m_editText = null; - private InputMethodManager m_imm = null; - private boolean m_quitApp = true; - private View m_dummyView = null; - private boolean m_keyboardIsVisible = false; - public boolean m_backKeyPressedSent = false; - private long m_showHideTimeStamp = System.nanoTime(); - private int m_portraitKeyboardHeight = 0; - private int m_landscapeKeyboardHeight = 0; - private int m_probeKeyboardHeightDelay = 50; // ms - private CursorHandle m_cursorHandle; - private CursorHandle m_leftSelectionHandle; - private CursorHandle m_rightSelectionHandle; - private EditPopupMenu m_editPopupMenu; - private boolean m_isPluginRunning = false; + private boolean m_backendsRegistered = false; + private View m_dummyView = null; + private final HashMap<Integer, View> m_nativeViews = new HashMap<>(); private QtAccessibilityDelegate m_accessibilityDelegate = null; - - public void setSystemUiVisibility(int systemUiVisibility) + QtActivityDelegate(Activity activity) { - if (m_systemUiVisibility == systemUiVisibility) - return; - - m_systemUiVisibility = systemUiVisibility; - setLayoutInDisplayCutoutMode(); - - int systemUiVisibilityFlags = View.SYSTEM_UI_FLAG_VISIBLE; - switch (m_systemUiVisibility) { - case SYSTEM_UI_VISIBILITY_NORMAL: - m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - break; - case SYSTEM_UI_VISIBILITY_FULLSCREEN: - m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - systemUiVisibilityFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.INVISIBLE; - break; - case SYSTEM_UI_VISIBILITY_TRANSLUCENT: - m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - break; - }; - - m_activity.getWindow().getDecorView().setSystemUiVisibility(systemUiVisibilityFlags); + super(activity); - m_layout.requestLayout(); + setActionBarVisibility(false); + setActivityBackgroundDrawable(); } - private void setLayoutInDisplayCutoutMode() + void registerBackends() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - int cutOutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; - if (SYSTEM_UI_VISIBILITY_FULLSCREEN == m_systemUiVisibility) - cutOutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - - m_activity.getWindow().getAttributes().layoutInDisplayCutoutMode = cutOutMode; + if (!m_backendsRegistered) { + m_backendsRegistered = true; + BackendRegister.registerBackend(QtWindowInterface.class, + (QtWindowInterface)QtActivityDelegate.this); + BackendRegister.registerBackend(QtAccessibilityInterface.class, + (QtAccessibilityInterface)QtActivityDelegate.this); + BackendRegister.registerBackend(QtMenuInterface.class, + (QtMenuInterface)QtActivityDelegate.this); + BackendRegister.registerBackend(QtLayoutInterface.class, + (QtLayoutInterface)QtActivityDelegate.this); + BackendRegister.registerBackend(QtInputInterface.class, + (QtInputInterface)m_inputDelegate); } } - public void updateFullScreen() + void unregisterBackends() { - if (m_systemUiVisibility == SYSTEM_UI_VISIBILITY_FULLSCREEN) { - m_systemUiVisibility = SYSTEM_UI_VISIBILITY_NORMAL; - setSystemUiVisibility(SYSTEM_UI_VISIBILITY_FULLSCREEN); + if (m_backendsRegistered) { + m_backendsRegistered = false; + BackendRegister.unregisterBackend(QtWindowInterface.class); + BackendRegister.unregisterBackend(QtAccessibilityInterface.class); + BackendRegister.unregisterBackend(QtMenuInterface.class); + BackendRegister.unregisterBackend(QtLayoutInterface.class); + BackendRegister.unregisterBackend(QtInputInterface.class); } } - public boolean isKeyboardVisible() + @Override + public QtLayout getQtLayout() { - return m_keyboardIsVisible; + return m_layout; } - // input method hints - must be kept in sync with QTDIR/src/corelib/global/qnamespace.h - private final int ImhHiddenText = 0x1; - private final int ImhSensitiveData = 0x2; - private final int ImhNoAutoUppercase = 0x4; - private final int ImhPreferNumbers = 0x8; - private final int ImhPreferUppercase = 0x10; - private final int ImhPreferLowercase = 0x20; - private final int ImhNoPredictiveText = 0x40; - - private final int ImhDate = 0x80; - private final int ImhTime = 0x100; - - private final int ImhPreferLatin = 0x200; - - private final int ImhMultiLine = 0x400; - - private final int ImhDigitsOnly = 0x10000; - private final int ImhFormattedNumbersOnly = 0x20000; - private final int ImhUppercaseOnly = 0x40000; - private final int ImhLowercaseOnly = 0x80000; - private final int ImhDialableCharactersOnly = 0x100000; - private final int ImhEmailCharactersOnly = 0x200000; - private final int ImhUrlCharactersOnly = 0x400000; - private final int ImhLatinOnly = 0x800000; - - // enter key type - must be kept in sync with QTDIR/src/corelib/global/qnamespace.h - private final int EnterKeyDefault = 0; - private final int EnterKeyReturn = 1; - private final int EnterKeyDone = 2; - private final int EnterKeyGo = 3; - private final int EnterKeySend = 4; - private final int EnterKeySearch = 5; - private final int EnterKeyNext = 6; - private final int EnterKeyPrevious = 7; - - // application state - public static final int ApplicationSuspended = 0x0; - public static final int ApplicationHidden = 0x1; - public static final int ApplicationInactive = 0x2; - public static final int ApplicationActive = 0x4; - - - public boolean setKeyboardVisibility(boolean visibility, long timeStamp) + @Override + public void setSystemUiVisibility(int systemUiVisibility) { - if (m_showHideTimeStamp > timeStamp) - return false; - m_showHideTimeStamp = timeStamp; + QtNative.runAction(() -> { + m_displayManager.setSystemUiVisibility(systemUiVisibility); + m_layout.requestLayout(); + QtNative.updateWindow(); + }); + } - if (m_keyboardIsVisible == visibility) - return false; - m_keyboardIsVisible = visibility; - QtNative.keyboardVisibilityUpdated(m_keyboardIsVisible); + @Override + public boolean updateActivityAfterRestart(Activity activity) { + boolean updated = super.updateActivityAfterRestart(activity); + // TODO verify whether this is even needed, the last I checked the initMembers + // recreates the layout anyway + // update the new activity content view to old layout + ViewGroup layoutParent = (ViewGroup)m_layout.getParent(); + if (layoutParent != null) + layoutParent.removeView(m_layout); - if (visibility == false) - updateFullScreen(); // Hiding the keyboard clears the immersive mode, so we need to set it again. + m_activity.setContentView(m_layout); - return true; - } - public void resetSoftwareKeyboard() - { - if (m_imm == null) - return; - m_editText.postDelayed(new Runnable() { - @Override - public void run() { - m_imm.restartInput(m_editText); - m_editText.m_optionsChanged = false; - } - }, 5); + return updated; } - public void showSoftwareKeyboard(final int x, final int y, final int width, final int height, final int inputHints, final int enterKeyType) + @Override + void startNativeApplicationImpl(String appParams, String mainLib) { - if (m_imm == null) - return; - - DisplayMetrics metrics = new DisplayMetrics(); - m_activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - // If the screen is in portrait mode than we estimate that keyboard height will not be higher than 2/5 of the screen. - // else than we estimate that keyboard height will not be higher than 2/3 of the screen - final int visibleHeight; - if (metrics.widthPixels < metrics.heightPixels) - visibleHeight = m_portraitKeyboardHeight != 0 ? m_portraitKeyboardHeight : metrics.heightPixels * 3 / 5; - else - visibleHeight = m_landscapeKeyboardHeight != 0 ? m_landscapeKeyboardHeight : metrics.heightPixels / 3; - - if (m_softInputMode != 0) { - m_activity.getWindow().setSoftInputMode(m_softInputMode); - final boolean softInputIsHidden = (m_softInputMode & WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) != 0; - if (softInputIsHidden) - return; - } else { - if (height > visibleHeight) - m_activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - else - m_activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - int initialCapsMode = 0; - - int imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE; - - switch (enterKeyType) { - case EnterKeyReturn: - imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION; - break; - case EnterKeyGo: - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_GO; - break; - case EnterKeySend: - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEND; - break; - case EnterKeySearch: - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH; - break; - case EnterKeyNext: - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_NEXT; - break; - case EnterKeyPrevious: - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS; - break; - } - - int inputType = android.text.InputType.TYPE_CLASS_TEXT; - - if ((inputHints & (ImhPreferNumbers | ImhDigitsOnly | ImhFormattedNumbersOnly)) != 0) { - inputType = android.text.InputType.TYPE_CLASS_NUMBER; - if ((inputHints & ImhFormattedNumbersOnly) != 0) { - inputType |= (android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL - | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED); - } - - if ((inputHints & ImhHiddenText) != 0) - inputType |= android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD; - } else if ((inputHints & ImhDialableCharactersOnly) != 0) { - inputType = android.text.InputType.TYPE_CLASS_PHONE; - } else if ((inputHints & (ImhDate | ImhTime)) != 0) { - inputType = android.text.InputType.TYPE_CLASS_DATETIME; - if ((inputHints & (ImhDate | ImhTime)) != (ImhDate | ImhTime)) { - if ((inputHints & ImhDate) != 0) - inputType |= android.text.InputType.TYPE_DATETIME_VARIATION_DATE; - else - inputType |= android.text.InputType.TYPE_DATETIME_VARIATION_TIME; - } // else { TYPE_DATETIME_VARIATION_NORMAL(0) } - } else { // CLASS_TEXT - if ((inputHints & ImhHiddenText) != 0) { - inputType |= android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD; - } else if ((inputHints & ImhSensitiveData) != 0 || - ((inputHints & ImhNoPredictiveText) != 0 && - System.getenv("QT_ANDROID_ENABLE_WORKAROUND_TO_DISABLE_PREDICTIVE_TEXT") != null)) { - inputType |= android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; - } else if ((inputHints & ImhUrlCharactersOnly) != 0) { - inputType |= android.text.InputType.TYPE_TEXT_VARIATION_URI; - if (enterKeyType == 0) // not explicitly overridden - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_GO; - } else if ((inputHints & ImhEmailCharactersOnly) != 0) { - inputType |= android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; - } - - if ((inputHints & ImhMultiLine) != 0) { - inputType |= android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE; - // Clear imeOptions for Multi-Line Type - // User should be able to insert new line in such case - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE; - } - if ((inputHints & (ImhNoPredictiveText | ImhSensitiveData | ImhHiddenText)) != 0) - inputType |= android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; - - if ((inputHints & ImhUppercaseOnly) != 0) { - initialCapsMode |= android.text.TextUtils.CAP_MODE_CHARACTERS; - inputType |= android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; - } else if ((inputHints & ImhLowercaseOnly) == 0 && (inputHints & ImhNoAutoUppercase) == 0) { - initialCapsMode |= android.text.TextUtils.CAP_MODE_SENTENCES; - inputType |= android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; - } - } - - if (enterKeyType == 0 && (inputHints & ImhMultiLine) != 0) - imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION; - - m_editText.setInitialCapsMode(initialCapsMode); - m_editText.setImeOptions(imeOptions); - m_editText.setInputType(inputType); - - m_layout.setLayoutParams(m_editText, new QtLayout.LayoutParams(width, height, x, y), false); - m_editText.requestFocus(); - m_editText.postDelayed(new Runnable() { - @Override - public void run() { - m_imm.showSoftInput(m_editText, 0, new ResultReceiver(new Handler()) { + m_layout.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - switch (resultCode) { - case InputMethodManager.RESULT_SHOWN: - QtNativeInputConnection.updateCursorPosition(); - //FALLTHROUGH - case InputMethodManager.RESULT_UNCHANGED_SHOWN: - setKeyboardVisibility(true, System.nanoTime()); - if (m_softInputMode == 0) { - // probe for real keyboard height - m_layout.postDelayed(new Runnable() { - @Override - public void run() { - if (!m_keyboardIsVisible) - return; - DisplayMetrics metrics = new DisplayMetrics(); - m_activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - Rect r = new Rect(); - m_activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); - if (metrics.heightPixels != r.bottom) { - if (metrics.widthPixels > metrics.heightPixels) { // landscape - if (m_landscapeKeyboardHeight != r.bottom) { - m_landscapeKeyboardHeight = r.bottom; - showSoftwareKeyboard(x, y, width, height, inputHints, enterKeyType); - } - } else { - if (m_portraitKeyboardHeight != r.bottom) { - m_portraitKeyboardHeight = r.bottom; - showSoftwareKeyboard(x, y, width, height, inputHints, enterKeyType); - } - } - } else { - // no luck ? - // maybe the delay was too short, so let's make it longer - if (m_probeKeyboardHeightDelay < 1000) - m_probeKeyboardHeightDelay *= 2; - } - } - }, m_probeKeyboardHeightDelay); - } - break; - case InputMethodManager.RESULT_HIDDEN: - case InputMethodManager.RESULT_UNCHANGED_HIDDEN: - setKeyboardVisibility(false, System.nanoTime()); - break; - } + public void onGlobalLayout() { + QtNative.startApplication(appParams, mainLib); + m_layout.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); - if (m_editText.m_optionsChanged) { - m_imm.restartInput(m_editText); - m_editText.m_optionsChanged = false; - } - } - }, 15); - } - - public void hideSoftwareKeyboard() - { - if (m_imm == null) - return; - m_imm.hideSoftInputFromWindow(m_editText.getWindowToken(), 0, new ResultReceiver(new Handler()) { - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - switch (resultCode) { - case InputMethodManager.RESULT_SHOWN: - case InputMethodManager.RESULT_UNCHANGED_SHOWN: - setKeyboardVisibility(true, System.nanoTime()); - break; - case InputMethodManager.RESULT_HIDDEN: - case InputMethodManager.RESULT_UNCHANGED_HIDDEN: - setKeyboardVisibility(false, System.nanoTime()); - break; - } - } - }); - } - - int getAppIconSize(Activity a) - { - int size = a.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); - if (size < 36 || size > 512) { // check size sanity - DisplayMetrics metrics = new DisplayMetrics(); - a.getWindowManager().getDefaultDisplay().getMetrics(metrics); - size = metrics.densityDpi / 10 * 3; - if (size < 36) - size = 36; - - if (size > 512) - size = 512; - } - - return size; - } - - public void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd) - { - if (m_imm == null) - return; - - m_imm.updateSelection(m_editText, selStart, selEnd, candidatesStart, candidatesEnd); - } - - // Values coming from QAndroidInputContext::CursorHandleShowMode - private static final int CursorHandleNotShown = 0; - private static final int CursorHandleShowNormal = 1; - private static final int CursorHandleShowSelection = 2; - private static final int CursorHandleShowEdit = 0x100; - - public int getSelectHandleWidth() - { - int width = 0; - if (m_leftSelectionHandle != null && m_rightSelectionHandle != null) { - width = Math.max(m_leftSelectionHandle.width(), m_rightSelectionHandle.width()); - } else if (m_cursorHandle != null) { - width = m_cursorHandle.width(); - } - return width; - } - - /* called from the C++ code when the position of the cursor or selection handles needs to - be adjusted. - mode is one of QAndroidInputContext::CursorHandleShowMode - */ - public void updateHandles(int mode, int editX, int editY, int editButtons, int x1, int y1, int x2, int y2, boolean rtl) - { - switch (mode & 0xff) - { - case CursorHandleNotShown: - if (m_cursorHandle != null) { - m_cursorHandle.hide(); - m_cursorHandle = null; - } - if (m_rightSelectionHandle != null) { - m_rightSelectionHandle.hide(); - m_leftSelectionHandle.hide(); - m_rightSelectionHandle = null; - m_leftSelectionHandle = null; - } - if (m_editPopupMenu != null) - m_editPopupMenu.hide(); - break; - - case CursorHandleShowNormal: - if (m_cursorHandle == null) { - m_cursorHandle = new CursorHandle(m_activity, m_layout, QtNative.IdCursorHandle, - android.R.attr.textSelectHandle, false); - } - m_cursorHandle.setPosition(x1, y1); - if (m_rightSelectionHandle != null) { - m_rightSelectionHandle.hide(); - m_leftSelectionHandle.hide(); - m_rightSelectionHandle = null; - m_leftSelectionHandle = null; - } - break; - - case CursorHandleShowSelection: - if (m_rightSelectionHandle == null) { - m_leftSelectionHandle = new CursorHandle(m_activity, m_layout, QtNative.IdLeftHandle, - !rtl ? android.R.attr.textSelectHandleLeft : - android.R.attr.textSelectHandleRight, - rtl); - m_rightSelectionHandle = new CursorHandle(m_activity, m_layout, QtNative.IdRightHandle, - !rtl ? android.R.attr.textSelectHandleRight : - android.R.attr.textSelectHandleLeft, - rtl); - } - m_leftSelectionHandle.setPosition(x1,y1); - m_rightSelectionHandle.setPosition(x2,y2); - if (m_cursorHandle != null) { - m_cursorHandle.hide(); - m_cursorHandle = null; - } - mode |= CursorHandleShowEdit; - break; - } - - if (!QtNative.hasClipboardText()) - editButtons &= ~EditContextView.PASTE_BUTTON; - - if ((mode & CursorHandleShowEdit) == CursorHandleShowEdit && editButtons != 0) { - m_editPopupMenu.setPosition(editX, editY, editButtons, m_cursorHandle, m_leftSelectionHandle, - m_rightSelectionHandle); - } else { - if (m_editPopupMenu != null) - m_editPopupMenu.hide(); - } } - private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() + @Override + protected void setUpLayout() { - @Override - public void onDisplayAdded(int displayId) { - QtNative.handleScreenAdded(displayId); - } - - private boolean isSimilarRotation(int r1, int r2) - { - return (r1 == r2) - || (r1 == Surface.ROTATION_0 && r2 == Surface.ROTATION_180) - || (r1 == Surface.ROTATION_180 && r2 == Surface.ROTATION_0) - || (r1 == Surface.ROTATION_90 && r2 == Surface.ROTATION_270) - || (r1 == Surface.ROTATION_270 && r2 == Surface.ROTATION_90); - } - - @Override - public void onDisplayChanged(int displayId) - { - Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) - ? m_activity.getWindowManager().getDefaultDisplay() - : m_activity.getDisplay(); - m_currentRotation = display.getRotation(); - m_layout.setActivityDisplayRotation(m_currentRotation); - // Process orientation change only if it comes after the size - // change, or if the screen is rotated by 180 degrees. - // Otherwise it will be processed in QtLayout. - if (isSimilarRotation(m_currentRotation, m_layout.displayRotation())) - QtNative.handleOrientationChanged(m_currentRotation, m_nativeOrientation); - - float refreshRate = display.getRefreshRate(); - QtNative.handleRefreshRateChanged(refreshRate); - QtNative.handleScreenChanged(displayId); - } - - @Override - public void onDisplayRemoved(int displayId) { - QtNative.handleScreenRemoved(displayId); - } - }; - - public boolean updateActivity(Activity activity) - { - try { - // set new activity - loadActivity(activity); - - // update the new activity content view to old layout - ViewGroup layoutParent = (ViewGroup)m_layout.getParent(); - if (layoutParent != null) - layoutParent.removeView(m_layout); - - m_activity.setContentView(m_layout); - - // force c++ native activity object to update - return QtNative.updateNativeActivity(); - } catch (Exception e) { - Log.w(QtNative.QtTAG, "Failed to update the activity."); - e.printStackTrace(); - return false; - } - } - - private void loadActivity(Activity activity) - throws NoSuchMethodException, PackageManager.NameNotFoundException - { - m_activity = activity; - - QtNative.setActivity(m_activity, this); - setActionBarVisibility(false); - - Class<?> activityClass = m_activity.getClass(); - m_super_dispatchKeyEvent = - activityClass.getMethod("super_dispatchKeyEvent", KeyEvent.class); - m_super_onRestoreInstanceState = - activityClass.getMethod("super_onRestoreInstanceState", Bundle.class); - m_super_onRetainNonConfigurationInstance = - activityClass.getMethod("super_onRetainNonConfigurationInstance"); - m_super_onSaveInstanceState = - activityClass.getMethod("super_onSaveInstanceState", Bundle.class); - m_super_onKeyDown = - activityClass.getMethod("super_onKeyDown", Integer.TYPE, KeyEvent.class); - m_super_onKeyUp = - activityClass.getMethod("super_onKeyUp", Integer.TYPE, KeyEvent.class); - m_super_onConfigurationChanged = - activityClass.getMethod("super_onConfigurationChanged", Configuration.class); - m_super_onActivityResult = - activityClass.getMethod("super_onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class); - m_super_onWindowFocusChanged = - activityClass.getMethod("super_onWindowFocusChanged", Boolean.TYPE); - m_super_dispatchGenericMotionEvent = - activityClass.getMethod("super_dispatchGenericMotionEvent", MotionEvent.class); - - m_softInputMode = m_activity.getPackageManager().getActivityInfo(m_activity.getComponentName(), 0).softInputMode; - - DisplayManager displayManager = (DisplayManager)m_activity.getSystemService(Context.DISPLAY_SERVICE); - displayManager.registerDisplayListener(displayListener, null); - } + int orientation = m_activity.getResources().getConfiguration().orientation; + m_layout = new QtRootLayout(m_activity); - public boolean loadApplication(Activity activity, ClassLoader classLoader, Bundle loaderParams) - { - /// check parameters integrity - if (!loaderParams.containsKey(NATIVE_LIBRARIES_KEY) - || !loaderParams.containsKey(BUNDLED_LIBRARIES_KEY) - || !loaderParams.containsKey(ENVIRONMENT_VARIABLES_KEY)) { - return false; - } + setUpSplashScreen(orientation); + m_activity.registerForContextMenu(m_layout); + m_activity.setContentView(m_layout, + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + QtDisplayManager.handleOrientationChanges(m_activity); - try { - loadActivity(activity); - QtNative.setClassLoader(classLoader); - } catch (Exception e) { - e.printStackTrace(); - return false; - } + handleUiModeChange(m_activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK); - if (loaderParams.containsKey(STATIC_INIT_CLASSES_KEY)) { - for (String className: Objects.requireNonNull(loaderParams.getStringArray(STATIC_INIT_CLASSES_KEY))) { - if (className.length() == 0) - continue; + Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + ? m_activity.getWindowManager().getDefaultDisplay() + : m_activity.getDisplay(); + QtDisplayManager.handleRefreshRateChanged(QtDisplayManager.getRefreshRate(display)); - try { - Class<?> initClass = classLoader.loadClass(className); - Object staticInitDataObject = initClass.newInstance(); // create an instance - try { - Method m = initClass.getMethod("setActivity", Activity.class, Object.class); - m.invoke(staticInitDataObject, m_activity, this); - } catch (Exception e) { - Log.d(QtNative.QtTAG, "Class " + className + " does not implement setActivity method"); - } + m_layout.getViewTreeObserver().addOnPreDrawListener(() -> { + if (!m_inputDelegate.isKeyboardVisible()) + return true; - // For modules that don't need/have setActivity - try { - Method m = initClass.getMethod("setContext", Context.class); - m.invoke(staticInitDataObject, (Context)m_activity); - } catch (Exception e) { - e.printStackTrace(); - } - } catch (Exception e) { - e.printStackTrace(); - } + Rect r = new Rect(); + m_activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); + DisplayMetrics metrics = new DisplayMetrics(); + m_activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + final int kbHeight = metrics.heightPixels - r.bottom; + if (kbHeight < 0) { + m_inputDelegate.setKeyboardVisibility(false, System.nanoTime()); + return true; } - } - QtNative.loadQtLibraries(loaderParams.getStringArrayList(NATIVE_LIBRARIES_KEY)); - ArrayList<String> libraries = loaderParams.getStringArrayList(BUNDLED_LIBRARIES_KEY); - String nativeLibsDir = QtNativeLibrariesDir.nativeLibrariesDir(m_activity); - QtNative.loadBundledLibraries(libraries, nativeLibsDir); - m_mainLib = loaderParams.getString(MAIN_LIBRARY_KEY); - // older apps provide the main library as the last bundled library; look for this if the main library isn't provided - if (null == m_mainLib && libraries.size() > 0) { - m_mainLib = libraries.get(libraries.size() - 1); - libraries.remove(libraries.size() - 1); - } - - ExtractStyle.setup(loaderParams); - ExtractStyle.runIfNeeded(m_activity, isUiModeDark(m_activity.getResources().getConfiguration())); - - QtNative.setEnvironmentVariables(loaderParams.getString(ENVIRONMENT_VARIABLES_KEY)); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", - "Droid Sans Mono;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif"); - QtNative.setEnvironmentVariable("HOME", m_activity.getFilesDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("TMPDIR", m_activity.getCacheDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS", - "Roboto;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_APP_ICON_SIZE", - String.valueOf(getAppIconSize(activity))); - - if (loaderParams.containsKey(APPLICATION_PARAMETERS_KEY)) - m_applicationParameters = loaderParams.getString(APPLICATION_PARAMETERS_KEY); - else - m_applicationParameters = ""; - - m_mainLib = QtNative.loadMainLibrary(m_mainLib, nativeLibsDir); - return m_mainLib != null; - } - - public boolean startApplication() - { - // start application - try { - - Bundle extras = m_activity.getIntent().getExtras(); - if (extras != null) { - try { - final boolean isDebuggable = (m_activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; - if (!isDebuggable) - throw new Exception(); - - if (extras.containsKey("extraenvvars")) { - try { - QtNative.setEnvironmentVariables(new String( - Base64.decode(extras.getString("extraenvvars"), Base64.DEFAULT), - "UTF-8")); - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (extras.containsKey("extraappparams")) { - try { - m_applicationParameters += "\t" + new String(Base64.decode(extras.getString("extraappparams"), Base64.DEFAULT), "UTF-8"); - } catch (Exception e) { - e.printStackTrace(); - } - } - } catch (Exception e) { - Log.e(QtNative.QtTAG, "Not in debug mode! It is not allowed to use " + - "extra arguments in non-debug mode."); - // This is not an error, so keep it silent - // e.printStackTrace(); - } - } // extras != null - - if (null == m_surfaces) - onCreate(null); + final int[] location = new int[2]; + m_layout.getLocationOnScreen(location); + QtInputDelegate.keyboardGeometryChanged(location[0], r.bottom - location[1], + r.width(), kbHeight); return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public void onTerminate() - { - QtNative.terminateQt(); - QtNative.m_qtThread.exit(); + }); + registerGlobalFocusChangeListener(m_layout); + m_inputDelegate.setEditPopupMenu(new EditPopupMenu(m_activity, m_layout)); } - public void onCreate(Bundle savedInstanceState) + @Override + protected void setUpSplashScreen(int orientation) { - m_quitApp = true; - Runnable startApplication = null; - if (null == savedInstanceState) { - startApplication = new Runnable() { - @Override - public void run() { - try { - QtNative.startApplication(m_applicationParameters, m_mainLib); - m_started = true; - } catch (Exception e) { - e.printStackTrace(); - m_activity.finish(); - } - } - }; - } - m_layout = new QtLayout(m_activity, startApplication); - - int orientation = m_activity.getResources().getConfiguration().orientation; - try { - ActivityInfo info = m_activity.getPackageManager().getActivityInfo(m_activity.getComponentName(), PackageManager.GET_META_DATA); + ActivityInfo info = m_activity.getPackageManager().getActivityInfo( + m_activity.getComponentName(), + PackageManager.GET_META_DATA); String splashScreenKey = "android.app.splash_screen_drawable_" + (orientation == Configuration.ORIENTATION_LANDSCAPE ? "landscape" : "portrait"); @@ -829,112 +182,70 @@ public class QtActivityDelegate splashScreenKey = "android.app.splash_screen_drawable"; if (info.metaData.containsKey(splashScreenKey)) { - m_splashScreenSticky = info.metaData.containsKey("android.app.splash_screen_sticky") && info.metaData.getBoolean("android.app.splash_screen_sticky"); + m_splashScreenSticky = + info.metaData.containsKey("android.app.splash_screen_sticky") && + info.metaData.getBoolean("android.app.splash_screen_sticky"); + int id = info.metaData.getInt(splashScreenKey); m_splashScreen = new ImageView(m_activity); - m_splashScreen.setImageDrawable(m_activity.getResources().getDrawable(id, m_activity.getTheme())); + m_splashScreen.setImageDrawable(m_activity.getResources().getDrawable( + id, m_activity.getTheme())); m_splashScreen.setScaleType(ImageView.ScaleType.FIT_XY); - m_splashScreen.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + m_splashScreen.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); m_layout.addView(m_splashScreen); } } catch (Exception e) { e.printStackTrace(); } - - m_editText = new QtEditText(m_activity, this); - m_imm = (InputMethodManager)m_activity.getSystemService(Context.INPUT_METHOD_SERVICE); - m_surfaces = new HashMap<Integer, QtSurface>(); - m_nativeViews = new HashMap<Integer, View>(); - m_activity.registerForContextMenu(m_layout); - m_activity.setContentView(m_layout, - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - int rotation = m_activity.getWindowManager().getDefaultDisplay().getRotation(); - boolean rot90 = (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270); - boolean currentlyLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE); - if ((currentlyLandscape && !rot90) || (!currentlyLandscape && rot90)) - m_nativeOrientation = Configuration.ORIENTATION_LANDSCAPE; - else - m_nativeOrientation = Configuration.ORIENTATION_PORTRAIT; - - m_layout.setNativeOrientation(m_nativeOrientation); - QtNative.handleOrientationChanged(rotation, m_nativeOrientation); - m_currentRotation = rotation; - - handleUiModeChange(m_activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK); - - float refreshRate = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) - ? m_activity.getWindowManager().getDefaultDisplay().getRefreshRate() - : m_activity.getDisplay().getRefreshRate(); - QtNative.handleRefreshRateChanged(refreshRate); - - m_layout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (!m_keyboardIsVisible) - return true; - - Rect r = new Rect(); - m_activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); - DisplayMetrics metrics = new DisplayMetrics(); - m_activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - final int kbHeight = metrics.heightPixels - r.bottom; - if (kbHeight < 0) { - setKeyboardVisibility(false, System.nanoTime()); - return true; - } - final int[] location = new int[2]; - m_layout.getLocationOnScreen(location); - QtNative.keyboardGeometryChanged(location[0], r.bottom - location[1], - r.width(), kbHeight); - return true; - } - }); - m_editPopupMenu = new EditPopupMenu(m_activity, m_layout); } - public void hideSplashScreen() + @Override + protected void hideSplashScreen(final int duration) { - hideSplashScreen(0); - } + QtNative.runAction(() -> { + if (m_splashScreen == null) + return; - public void hideSplashScreen(final int duration) - { - if (m_splashScreen == null) - return; + if (duration <= 0) { + m_layout.removeView(m_splashScreen); + m_splashScreen = null; + return; + } - if (duration <= 0) { - m_layout.removeView(m_splashScreen); - m_splashScreen = null; - return; - } + final Animation fadeOut = new AlphaAnimation(1, 0); + fadeOut.setInterpolator(new AccelerateInterpolator()); + fadeOut.setDuration(duration); - final Animation fadeOut = new AlphaAnimation(1, 0); - fadeOut.setInterpolator(new AccelerateInterpolator()); - fadeOut.setDuration(duration); + fadeOut.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + hideSplashScreen(0); + } - fadeOut.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationEnd(Animation animation) { hideSplashScreen(0); } + @Override + public void onAnimationRepeat(Animation animation) { + } - @Override - public void onAnimationRepeat(Animation animation) {} + @Override + public void onAnimationStart(Animation animation) { + } + }); - @Override - public void onAnimationStart(Animation animation) {} + m_splashScreen.startAnimation(fadeOut); }); - - m_splashScreen.startAnimation(fadeOut); } - public void notifyAccessibilityLocationChange(int viewId) + @Override + public void notifyLocationChange(int viewId) { if (m_accessibilityDelegate == null) return; m_accessibilityDelegate.notifyLocationChange(viewId); } + @Override public void notifyObjectHide(int viewId, int parentId) { if (m_accessibilityDelegate == null) @@ -942,6 +253,15 @@ public class QtActivityDelegate m_accessibilityDelegate.notifyObjectHide(viewId, parentId); } + @Override + public void notifyObjectShow(int parentId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyObjectShow(parentId); + } + + @Override public void notifyObjectFocus(int viewId) { if (m_accessibilityDelegate == null) @@ -949,6 +269,7 @@ public class QtActivityDelegate m_accessibilityDelegate.notifyObjectFocus(viewId); } + @Override public void notifyValueChanged(int viewId, String value) { if (m_accessibilityDelegate == null) @@ -956,6 +277,7 @@ public class QtActivityDelegate m_accessibilityDelegate.notifyValueChanged(viewId, value); } + @Override public void notifyScrolledEvent(int viewId) { if (m_accessibilityDelegate == null) @@ -963,268 +285,59 @@ public class QtActivityDelegate m_accessibilityDelegate.notifyScrolledEvent(viewId); } - - public void notifyQtAndroidPluginRunning(boolean running) - { - m_isPluginRunning = running; - } - + @Override public void initializeAccessibility() { - m_accessibilityDelegate = new QtAccessibilityDelegate(m_activity, m_layout, this); - } - - public void onWindowFocusChanged(boolean hasFocus) { - try { - m_super_onWindowFocusChanged.invoke(m_activity, hasFocus); - } catch (Exception e) { - e.printStackTrace(); - } - if (hasFocus) - updateFullScreen(); - } - - boolean isUiModeDark(Configuration config) - { - return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - } - - private void handleUiModeChange(int uiMode) - { - // QTBUG-108365 - if (Build.VERSION.SDK_INT >= 30) { - // Since 29 version we are using Theme_DeviceDefault_DayNight - Window window = m_activity.getWindow(); - WindowInsetsController controller = window.getInsetsController(); - if (controller != null) { - // set APPEARANCE_LIGHT_STATUS_BARS if needed - int appearanceLight = Color.luminance(window.getStatusBarColor()) > 0.5 ? - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS : 0; - controller.setSystemBarsAppearance(appearanceLight, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS); - } - } - switch (uiMode) { - case Configuration.UI_MODE_NIGHT_NO: - ExtractStyle.runIfNeeded(m_activity, false); - QtNative.handleUiDarkModeChanged(0); - break; - case Configuration.UI_MODE_NIGHT_YES: - ExtractStyle.runIfNeeded(m_activity, true); - QtNative.handleUiDarkModeChanged(1); - break; - } - } - - public void onConfigurationChanged(Configuration configuration) - { - try { - m_super_onConfigurationChanged.invoke(m_activity, configuration); - } catch (Exception e) { - e.printStackTrace(); - } - handleUiModeChange(configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK); - } - - public void onDestroy() - { - if (m_quitApp) { - QtNative.terminateQt(); - QtNative.setActivity(null, null); - QtNative.m_qtThread.exit(); - System.exit(0); - } - } - - public void onPause() - { - if (Build.VERSION.SDK_INT < 24 || !m_activity.isInMultiWindowMode()) - QtNative.setApplicationState(ApplicationInactive); - } - - public void onResume() - { - QtNative.setApplicationState(ApplicationActive); - if (m_started) { - QtNative.updateWindow(); - updateFullScreen(); // Suspending the app clears the immersive mode, so we need to set it again. - } - } - - public void onNewIntent(Intent data) - { - QtNative.onNewIntent(data); - } - - public void onActivityResult(int requestCode, int resultCode, Intent data) - { - try { - m_super_onActivityResult.invoke(m_activity, requestCode, resultCode, data); - } catch (Exception e) { - e.printStackTrace(); - } - - QtNative.onActivityResult(requestCode, resultCode, data); - } - - - public void onStop() - { - QtNative.setApplicationState(ApplicationSuspended); - } - - public Object onRetainNonConfigurationInstance() - { - try { - m_super_onRetainNonConfigurationInstance.invoke(m_activity); - } catch (Exception e) { - e.printStackTrace(); - } - m_quitApp = false; - return true; - } - - public void onSaveInstanceState(Bundle outState) { - try { - m_super_onSaveInstanceState.invoke(m_activity, outState); - } catch (Exception e) { - e.printStackTrace(); - } - outState.putInt("SystemUiVisibility", m_systemUiVisibility); - outState.putBoolean("Started", m_started); - // It should never - } - - public void onRestoreInstanceState(Bundle savedInstanceState) - { - try { - m_super_onRestoreInstanceState.invoke(m_activity, savedInstanceState); - } catch (Exception e) { - e.printStackTrace(); - } - m_started = savedInstanceState.getBoolean("Started"); - // FIXME restore all surfaces - - } - - public boolean onKeyDown(int keyCode, KeyEvent event) - { - if (!m_started || !m_isPluginRunning) - return false; - - m_metaState = MetaKeyKeyListener.handleKeyDown(m_metaState, keyCode, event); - int c = event.getUnicodeChar(MetaKeyKeyListener.getMetaState(m_metaState) | event.getMetaState()); - int lc = c; - m_metaState = MetaKeyKeyListener.adjustMetaAfterKeypress(m_metaState); - - if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { - c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; - int composed = KeyEvent.getDeadChar(m_lastChar, c); - c = composed; - } - - if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP - || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN - || keyCode == KeyEvent.KEYCODE_MUTE) - && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { - return false; - } - - m_lastChar = lc; - if (keyCode == KeyEvent.KEYCODE_BACK) { - m_backKeyPressedSent = !m_keyboardIsVisible; - if (!m_backKeyPressedSent) - return true; - } - QtNative.keyDown(keyCode, c, event.getMetaState(), event.getRepeatCount() > 0); - - return true; - } - - public boolean onKeyUp(int keyCode, KeyEvent event) - { - if (!m_started || !m_isPluginRunning) - return false; - - if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP - || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN - || keyCode == KeyEvent.KEYCODE_MUTE) - && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { - return false; - } - - if (keyCode == KeyEvent.KEYCODE_BACK && !m_backKeyPressedSent) { - hideSoftwareKeyboard(); - setKeyboardVisibility(false, System.nanoTime()); - return true; - } - - m_metaState = MetaKeyKeyListener.handleKeyUp(m_metaState, keyCode, event); - QtNative.keyUp(keyCode, event.getUnicodeChar(), event.getMetaState(), event.getRepeatCount() > 0); - return true; - } - - public boolean dispatchKeyEvent(KeyEvent event) - { - if (m_started - && event.getAction() == KeyEvent.ACTION_MULTIPLE - && event.getCharacters() != null - && event.getCharacters().length() == 1 - && event.getKeyCode() == 0) { - QtNative.keyDown(0, event.getCharacters().charAt(0), event.getMetaState(), event.getRepeatCount() > 0); - QtNative.keyUp(0, event.getCharacters().charAt(0), event.getMetaState(), event.getRepeatCount() > 0); - } - - if (QtNative.dispatchKeyEvent(event)) - return true; - - try { - return (Boolean) m_super_dispatchKeyEvent.invoke(m_activity, event); - } catch (Exception e) { - e.printStackTrace(); - } - return false; + QtNative.runAction(() -> { + // FIXME make QtAccessibilityDelegate window based + if (m_layout != null) + m_accessibilityDelegate = new QtAccessibilityDelegate(m_layout); + else + Log.w(QtTAG, "Null layout, failed to initialize accessibility delegate."); + }); } - private boolean m_optionsMenuIsVisible = false; - public boolean onCreateOptionsMenu(Menu menu) - { - menu.clear(); - return true; - } - public boolean onPrepareOptionsMenu(Menu menu) + // QtMenuInterface implementation begin + @Override + public void resetOptionsMenu() { - m_optionsMenuIsVisible = true; - boolean res = QtNative.onPrepareOptionsMenu(menu); - setActionBarVisibility(res && menu.size() > 0); - return res; + QtNative.runAction(() -> m_activity.invalidateOptionsMenu()); } - public boolean onOptionsItemSelected(MenuItem item) + @Override + public void openOptionsMenu() { - return QtNative.onOptionsItemSelected(item.getItemId(), item.isChecked()); + QtNative.runAction(() -> m_activity.openOptionsMenu()); } - public void onOptionsMenuClosed(Menu menu) + @Override + public void closeContextMenu() { - m_optionsMenuIsVisible = false; - QtNative.onOptionsMenuClosed(menu); + QtNative.runAction(() -> m_activity.closeContextMenu()); } - public void resetOptionsMenu() + @Override + public void openContextMenu(final int x, final int y, final int w, final int h) { - m_activity.invalidateOptionsMenu(); - } + m_layout.postDelayed(() -> { + final QtEditText focusedEditText = m_inputDelegate.getCurrentQtEditText(); + if (focusedEditText == null) { + Log.w(QtTAG, "No focused view when trying to open context menu"); + return; + } + m_layout.setLayoutParams(focusedEditText, new QtLayout.LayoutParams(w, h, x, y), false); + PopupMenu popup = new PopupMenu(m_activity, focusedEditText); + QtActivityDelegate.this.onCreatePopupMenu(popup.getMenu()); + popup.setOnMenuItemClickListener(menuItem -> + m_activity.onContextItemSelected(menuItem)); + popup.setOnDismissListener(popupMenu -> + m_activity.onContextMenuClosed(popupMenu.getMenu())); + popup.show(); + }, 100); + } + // QtMenuInterface implementation end private boolean m_contextMenuVisible = false; - public void onCreateContextMenu(ContextMenu menu, - View v, - ContextMenuInfo menuInfo) - { - menu.clearHeader(); - QtNative.onCreateContextMenu(menu); - m_contextMenuVisible = true; - } public void onCreatePopupMenu(Menu menu) { @@ -1232,51 +345,8 @@ public class QtActivityDelegate m_contextMenuVisible = true; } - public void onContextMenuClosed(Menu menu) - { - if (!m_contextMenuVisible) - return; - m_contextMenuVisible = false; - QtNative.onContextMenuClosed(menu); - } - - public boolean onContextItemSelected(MenuItem item) - { - m_contextMenuVisible = false; - return QtNative.onContextItemSelected(item.getItemId(), item.isChecked()); - } - - public void openContextMenu(final int x, final int y, final int w, final int h) - { - m_layout.postDelayed(new Runnable() { - @Override - public void run() { - m_layout.setLayoutParams(m_editText, new QtLayout.LayoutParams(w, h, x, y), false); - PopupMenu popup = new PopupMenu(m_activity, m_editText); - QtActivityDelegate.this.onCreatePopupMenu(popup.getMenu()); - popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - return QtActivityDelegate.this.onContextItemSelected(menuItem); - } - }); - popup.setOnDismissListener(new PopupMenu.OnDismissListener() { - @Override - public void onDismiss(PopupMenu popupMenu) { - QtActivityDelegate.this.onContextMenuClosed(popupMenu.getMenu()); - } - }); - popup.show(); - } - }, 100); - } - - public void closeContextMenu() - { - m_activity.closeContextMenu(); - } - - private void setActionBarVisibility(boolean visible) + @Override + void setActionBarVisibility(boolean visible) { if (m_activity.getActionBar() == null) return; @@ -1286,149 +356,127 @@ public class QtActivityDelegate m_activity.getActionBar().show(); } - public void insertNativeView(int id, View view, int x, int y, int w, int h) { - if (m_dummyView != null) { - m_layout.removeView(m_dummyView); - m_dummyView = null; - } - - if (m_nativeViews.containsKey(id)) - m_layout.removeView(m_nativeViews.remove(id)); - - if (w < 0 || h < 0) { - view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - } else { - view.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); - } - - view.setId(id); - m_layout.addView(view); - m_nativeViews.put(id, view); - } + @UsedFromNativeCode + @Override + public void addTopLevelWindow(final QtWindow window) + { + if (window == null) + return; - public void createSurface(int id, boolean onTop, int x, int y, int w, int h, int imageDepth) { - if (m_surfaces.size() == 0) { - TypedValue attr = new TypedValue(); - m_activity.getTheme().resolveAttribute(android.R.attr.windowBackground, attr, true); - if (attr.type >= TypedValue.TYPE_FIRST_COLOR_INT && attr.type <= TypedValue.TYPE_LAST_COLOR_INT) { - m_activity.getWindow().setBackgroundDrawable(new ColorDrawable(attr.data)); - } else { - m_activity.getWindow().setBackgroundDrawable(m_activity.getResources().getDrawable(attr.resourceId, m_activity.getTheme())); - } - if (m_dummyView != null) { - m_layout.removeView(m_dummyView); - m_dummyView = null; + QtNative.runAction(()-> { + if (m_topLevelWindows.size() == 0) { + if (m_dummyView != null) { + m_layout.removeView(m_dummyView); + m_dummyView = null; + } } - } - if (m_surfaces.containsKey(id)) - m_layout.removeView(m_surfaces.remove(id)); + window.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); - QtSurface surface = new QtSurface(m_activity, id, onTop, imageDepth); - if (w < 0 || h < 0) { - surface.setLayoutParams( new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - } else { - surface.setLayoutParams( new QtLayout.LayoutParams(w, h, x, y)); - } - - // Native views are always inserted in the end of the stack (i.e., on top). - // All other views are stacked based on the order they are created. - final int surfaceCount = getSurfaceCount(); - m_layout.addView(surface, surfaceCount); - - m_surfaces.put(id, surface); - if (!m_splashScreenSticky) - hideSplashScreen(); + m_layout.addView(window, m_topLevelWindows.size()); + m_topLevelWindows.put(window.getId(), window); + if (!m_splashScreenSticky) + hideSplashScreen(); + }); } - public void setSurfaceGeometry(int id, int x, int y, int w, int h) { - if (m_surfaces.containsKey(id)) { - QtSurface surface = m_surfaces.get(id); - surface.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); - } else if (m_nativeViews.containsKey(id)) { - View view = m_nativeViews.get(id); - view.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); - } else { - Log.e(QtNative.QtTAG, "Surface " + id +" not found!"); - return; - } + @UsedFromNativeCode + @Override + public void removeTopLevelWindow(final int id) + { + QtNative.runAction(()-> { + if (m_topLevelWindows.containsKey(id)) { + QtWindow window = m_topLevelWindows.remove(id); + if (m_topLevelWindows.isEmpty()) { + // Keep last frame in stack until it is replaced to get correct + // shutdown transition + m_dummyView = window; + } else { + m_layout.removeView(window); + } + } + }); } - public void destroySurface(int id) { - View view = null; - - if (m_surfaces.containsKey(id)) { - view = m_surfaces.remove(id); - } else if (m_nativeViews.containsKey(id)) { - view = m_nativeViews.remove(id); - } else { - Log.e(QtNative.QtTAG, "Surface " + id +" not found!"); - } - - if (view == null) - return; - - // Keep last frame in stack until it is replaced to get correct - // shutdown transition - if (m_surfaces.size() == 0 && m_nativeViews.size() == 0) { - m_dummyView = view; - } else { - m_layout.removeView(view); - } + @UsedFromNativeCode + @Override + public void bringChildToFront(final int id) + { + QtNative.runAction(() -> { + QtWindow window = m_topLevelWindows.get(id); + if (window != null) + m_layout.moveChild(window, m_topLevelWindows.size() - 1); + }); } - public int getSurfaceCount() + @UsedFromNativeCode + @Override + public void bringChildToBack(int id) { - return m_surfaces.size(); + QtNative.runAction(() -> { + QtWindow window = m_topLevelWindows.get(id); + if (window != null) + m_layout.moveChild(window, 0); + }); } - public void bringChildToFront(int id) + private void setActivityBackgroundDrawable() { - View view = m_surfaces.get(id); - if (view != null) { - final int surfaceCount = getSurfaceCount(); - if (surfaceCount > 0) - m_layout.moveChild(view, surfaceCount - 1); - return; + TypedValue attr = new TypedValue(); + m_activity.getTheme().resolveAttribute(android.R.attr.windowBackground, + attr, true); + Drawable backgroundDrawable; + if (attr.type >= TypedValue.TYPE_FIRST_COLOR_INT && + attr.type <= TypedValue.TYPE_LAST_COLOR_INT) { + backgroundDrawable = new ColorDrawable(attr.data); + } else { + backgroundDrawable = m_activity.getResources(). + getDrawable(attr.resourceId, m_activity.getTheme()); } - view = m_nativeViews.get(id); - if (view != null) - m_layout.moveChild(view, -1); + m_activity.getWindow().setBackgroundDrawable(backgroundDrawable); } - public void bringChildToBack(int id) + // TODO: QTBUG-122761 To be removed after QtAndroidAutomotive does not depend on it. + @UsedFromNativeCode + public void insertNativeView(int id, View view, int x, int y, int w, int h) { - View view = m_surfaces.get(id); - if (view != null) { - m_layout.moveChild(view, 0); - return; - } + QtNative.runAction(()-> { + if (m_dummyView != null) { + m_layout.removeView(m_dummyView); + m_dummyView = null; + } - view = m_nativeViews.get(id); - if (view != null) { - final int index = getSurfaceCount(); - m_layout.moveChild(view, index); - } - } + if (m_nativeViews.containsKey(id)) + m_layout.removeView(m_nativeViews.remove(id)); - public boolean dispatchGenericMotionEvent (MotionEvent ev) - { - if (m_started && QtNative.dispatchGenericMotionEvent(ev)) - return true; + if (w < 0 || h < 0) { + view.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } else { + view.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); + } - try { - return (Boolean) m_super_dispatchGenericMotionEvent.invoke(m_activity, ev); - } catch (Exception e) { - e.printStackTrace(); - } - return false; + view.setId(id); + m_layout.addView(view); + m_nativeViews.put(id, view); + }); } - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) + // TODO: QTBUG-122761 To be removed after QtAndroidAutomotive does not depend on it. + @UsedFromNativeCode + public void setNativeViewGeometry(int id, int x, int y, int w, int h) { - QtNative.sendRequestPermissionsResult(requestCode, permissions, grantResults); + QtNative.runAction(() -> { + if (m_nativeViews.containsKey(id)) { + View view = m_nativeViews.get(id); + if (view != null) + view.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); + } else { + Log.e(QtTAG, "View " + id + " not found!"); + } + }); } } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegateBase.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegateBase.java new file mode 100644 index 0000000000..4980a47d08 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegateBase.java @@ -0,0 +1,177 @@ +// Copyright (C) 2017 BogDan Vatra <bogdan@kde.org> +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com> +// 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.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.Rect; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.ViewTreeObserver; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.Menu; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsetsController; +import android.widget.ImageView; +import android.widget.PopupMenu; + +import java.util.HashMap; + +abstract class QtActivityDelegateBase +{ + protected Activity m_activity; + protected HashMap<Integer, QtWindow> m_topLevelWindows; + protected QtDisplayManager m_displayManager = null; + protected QtInputDelegate m_inputDelegate = null; + + private boolean m_membersInitialized = false; + private boolean m_contextMenuVisible = false; + + // Subclass must implement these + abstract void startNativeApplicationImpl(String appParams, String mainLib); + + // With these we are okay with default implementation doing nothing + void setUpLayout() {} + void setUpSplashScreen(int orientation) {} + void hideSplashScreen(final int duration) {} + void setActionBarVisibility(boolean visible) {} + + QtActivityDelegateBase(Activity activity) + { + m_activity = activity; + // Set native context + QtNative.setActivity(m_activity); + } + + QtDisplayManager displayManager() { + return m_displayManager; + } + + QtInputDelegate getInputDelegate() { + return m_inputDelegate; + } + + void setContextMenuVisible(boolean contextMenuVisible) + { + m_contextMenuVisible = contextMenuVisible; + } + + boolean isContextMenuVisible() + { + return m_contextMenuVisible; + } + + public boolean updateActivityAfterRestart(Activity activity) { + try { + // set new activity + m_activity = activity; + QtNative.setActivity(m_activity); + + // force c++ native activity object to update + return QtNative.updateNativeActivity(); + } catch (Exception e) { + Log.w(QtNative.QtTAG, "Failed to update the activity."); + e.printStackTrace(); + return false; + } + } + + public void startNativeApplication(String appParams, String mainLib) + { + if (m_membersInitialized) + return; + initMembers(); + startNativeApplicationImpl(appParams, mainLib); + } + + void initMembers() + { + m_membersInitialized = true; + m_topLevelWindows = new HashMap<Integer, QtWindow>(); + + m_displayManager = new QtDisplayManager(m_activity); + m_displayManager.registerDisplayListener(); + + QtInputDelegate.KeyboardVisibilityListener keyboardVisibilityListener = + () -> m_displayManager.updateFullScreen(); + m_inputDelegate = new QtInputDelegate(m_activity, keyboardVisibilityListener); + + try { + PackageManager pm = m_activity.getPackageManager(); + ActivityInfo activityInfo = pm.getActivityInfo(m_activity.getComponentName(), 0); + m_inputDelegate.setSoftInputMode(activityInfo.softInputMode); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + setUpLayout(); + } + + protected void registerGlobalFocusChangeListener(final View view) { + view.getViewTreeObserver().addOnGlobalFocusChangeListener(this::onGlobalFocusChanged); + } + + private void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (newFocus instanceof QtEditText) { + final QtWindow newWindow = (QtWindow) newFocus.getParent(); + QtWindow.windowFocusChanged(true, newWindow.getId()); + m_inputDelegate.setFocusedView((QtEditText) newFocus); + } else { + int id = -1; + if (oldFocus instanceof QtEditText) { + final QtWindow oldWindow = (QtWindow) oldFocus.getParent(); + id = oldWindow.getId(); + } + QtWindow.windowFocusChanged(false, id); + m_inputDelegate.setFocusedView(null); + } + } + + public void hideSplashScreen() + { + hideSplashScreen(0); + } + + void handleUiModeChange(int uiMode) + { + // QTBUG-108365 + if (Build.VERSION.SDK_INT >= 30) { + // Since 29 version we are using Theme_DeviceDefault_DayNight + Window window = m_activity.getWindow(); + WindowInsetsController controller = window.getInsetsController(); + if (controller != null) { + // set APPEARANCE_LIGHT_STATUS_BARS if needed + int appearanceLight = Color.luminance(window.getStatusBarColor()) > 0.5 ? + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS : 0; + controller.setSystemBarsAppearance(appearanceLight, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS); + } + } + switch (uiMode) { + case Configuration.UI_MODE_NIGHT_NO: + ExtractStyle.runIfNeeded(m_activity, false); + QtDisplayManager.handleUiDarkModeChanged(0); + break; + case Configuration.UI_MODE_NIGHT_YES: + ExtractStyle.runIfNeeded(m_activity, true); + QtDisplayManager.handleUiDarkModeChanged(1); + break; + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java new file mode 100644 index 0000000000..1b2e8e49a0 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java @@ -0,0 +1,151 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (c) 2016, BogDan Vatra <bogdan@kde.org> +// 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.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.Bundle; +import android.system.Os; +import android.util.Base64; +import android.util.DisplayMetrics; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +class QtActivityLoader extends QtLoader { + private final Activity m_activity; + + public QtActivityLoader(Activity activity) + { + super(new ContextWrapper(activity)); + m_activity = activity; + + extractContextMetaData(); + } + + private void showErrorDialog() { + if (m_activity == null) { + Log.w(QtTAG, "cannot show the error dialog from a null activity object"); + return; + } + Resources resources = m_activity.getResources(); + String packageName = m_activity.getPackageName(); + AlertDialog errorDialog = new AlertDialog.Builder(m_activity).create(); + @SuppressLint("DiscouragedApi") int id = resources.getIdentifier( + "fatal_error_msg", "string", packageName); + errorDialog.setMessage(resources.getString(id)); + errorDialog.setButton(Dialog.BUTTON_POSITIVE, resources.getString(android.R.string.ok), + (dialog, which) -> finish()); + errorDialog.show(); + } + + @Override + protected void finish() { + if (m_activity == null) { + Log.w(QtTAG, "finish() called when activity object is null"); + return; + } + showErrorDialog(); + m_activity.finish(); + } + + private String getDecodedUtfString(String str) + { + byte[] decodedExtraEnvVars = Base64.decode(str, Base64.DEFAULT); + return new String(decodedExtraEnvVars, StandardCharsets.UTF_8); + } + + int getAppIconSize() + { + int size = m_activity.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + if (size < 36 || size > 512) { // check size sanity + DisplayMetrics metrics = new DisplayMetrics(); + m_activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + size = metrics.densityDpi / 10 * 3; + if (size < 36) + size = 36; + + if (size > 512) + size = 512; + } + + return size; + } + + private void setupStyleExtraction() + { + int displayDensity = m_activity.getResources().getDisplayMetrics().densityDpi; + setEnvironmentVariable("QT_ANDROID_THEME_DISPLAY_DPI", String.valueOf(displayDensity)); + + String extractOption = getMetaData("android.app.extract_android_style"); + if (extractOption.equals("full")) + setEnvironmentVariable("QT_USE_ANDROID_NATIVE_STYLE", String.valueOf(1)); + + String stylePath = ExtractStyle.setup(m_activity, extractOption, displayDensity); + setEnvironmentVariable("ANDROID_STYLE_PATH", stylePath); + } + + @Override + protected void extractContextMetaData() + { + super.extractContextMetaData(); + + setEnvironmentVariable("QT_USE_ANDROID_NATIVE_DIALOGS", String.valueOf(1)); + setEnvironmentVariable("QT_ANDROID_APP_ICON_SIZE", String.valueOf(getAppIconSize())); + + setupStyleExtraction(); + + Intent intent = m_activity.getIntent(); + if (intent == null) { + Log.w(QtTAG, "Null Intent from the current Activity."); + return; + } + + String intentArgs = intent.getStringExtra("applicationArguments"); + if (intentArgs != null) + appendApplicationParameters(intentArgs); + + Bundle extras = intent.getExtras(); + if (extras == null) { + Log.w(QtTAG, "Null extras from the Activity's intent."); + return; + } + + int flags = m_activity.getApplicationInfo().flags; + boolean isDebuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + + if (isDebuggable) { + if (extras.containsKey("extraenvvars")) { + String extraEnvVars = extras.getString("extraenvvars"); + setEnvironmentVariables(getDecodedUtfString(extraEnvVars)); + } + + if (extras.containsKey("extraappparams")) { + String extraAppParams = extras.getString("extraappparams"); + appendApplicationParameters(getDecodedUtfString(extraAppParams)); + } + + m_debuggerSleepMs = 3000; + if (Os.getenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS") != null) { + try { + m_debuggerSleepMs = Integer.parseInt(Os.getenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS")); + } catch (NumberFormatException ignored) { + } + } + } else { + Log.d(QtNative.QtTAG, "Not in debug mode! It is not allowed to use extra arguments " + + "in non-debug mode."); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtApplicationBase.java b/src/android/jar/src/org/qtproject/qt/android/QtApplicationBase.java new file mode 100644 index 0000000000..de572266b9 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtApplicationBase.java @@ -0,0 +1,15 @@ +// 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 + +package org.qtproject.qt.android; + +import android.app.Application; + +public class QtApplicationBase extends Application { + @Override + public void onTerminate() { + QtNative.terminateQt(); + QtNative.getQtThread().exit(); + super.onTerminate(); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtClipboardManager.java b/src/android/jar/src/org/qtproject/qt/android/QtClipboardManager.java new file mode 100644 index 0000000000..ac0d4e1890 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtClipboardManager.java @@ -0,0 +1,232 @@ +// 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 + +package org.qtproject.qt.android; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.concurrent.Semaphore; + +class QtClipboardManager +{ + public static native void onClipboardDataChanged(long nativePointer); + + private final static String TAG = "QtClipboardManager"; + private ClipboardManager m_clipboardManager = null; + private boolean m_usePrimaryClip = false; + private final long m_nativePointer; + + public QtClipboardManager(Context context, long nativePointer) + { + m_nativePointer = nativePointer; + registerClipboardManager(context); + } + + private void registerClipboardManager(Context context) + { + if (context != null) { + final Semaphore semaphore = new Semaphore(0); + QtNative.runAction(() -> { + m_clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (m_clipboardManager != null) { + m_clipboardManager.addPrimaryClipChangedListener( + () -> onClipboardDataChanged(m_nativePointer)); + } + semaphore.release(); + }); + try { + semaphore.acquire(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @UsedFromNativeCode + public void clearClipData() + { + if (m_clipboardManager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + m_clipboardManager.clearPrimaryClip(); + } else { + String[] mimeTypes = { "application/octet-stream" }; + ClipData data = new ClipData("", mimeTypes, new ClipData.Item(new Intent())); + m_clipboardManager.setPrimaryClip(data); + } + } + m_usePrimaryClip = false; + } + + @UsedFromNativeCode + public void setClipboardText(Context context, String text) + { + if (m_clipboardManager != null) { + ClipData clipData = ClipData.newPlainText("text/plain", text); + updatePrimaryClip(clipData, context); + } + } + + public static boolean hasClipboardText(Context context) + { + ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + if (clipboardManager == null) + return false; + + ClipDescription description = clipboardManager.getPrimaryClipDescription(); + // getPrimaryClipDescription can fail if the app does not have input focus + if (description == null) + return false; + + for (int i = 0; i < description.getMimeTypeCount(); ++i) { + String itemMimeType = description.getMimeType(i); + if (itemMimeType.matches("text/(.*)")) + return true; + } + return false; + } + + @UsedFromNativeCode + public boolean hasClipboardText() + { + return hasClipboardMimeType("text/(.*)"); + } + + @UsedFromNativeCode + public String getClipboardText() + { + try { + if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { + ClipData primaryClip = m_clipboardManager.getPrimaryClip(); + if (primaryClip != null) { + for (int i = 0; i < primaryClip.getItemCount(); ++i) + if (primaryClip.getItemAt(i).getText() != null) + return primaryClip.getItemAt(i).getText().toString(); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get clipboard data", e); + } + return ""; + } + + private void updatePrimaryClip(ClipData clipData, Context context) + { + try { + if (m_usePrimaryClip) { + ClipData clip = m_clipboardManager.getPrimaryClip(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Objects.requireNonNull(clip).addItem(context.getContentResolver(), + clipData.getItemAt(0)); + } else { + Objects.requireNonNull(clip).addItem(clipData.getItemAt(0)); + } + m_clipboardManager.setPrimaryClip(clip); + } else { + m_clipboardManager.setPrimaryClip(clipData); + m_usePrimaryClip = true; + } + } catch (Exception e) { + Log.e(TAG, "Failed to set clipboard data", e); + } + } + + @UsedFromNativeCode + public void setClipboardHtml(Context context, String text, String html) + { + if (m_clipboardManager != null) { + ClipData clipData = ClipData.newHtmlText("text/html", text, html); + updatePrimaryClip(clipData, context); + } + } + + private boolean hasClipboardMimeType(String mimeType) + { + if (m_clipboardManager == null) + return false; + + ClipDescription description = m_clipboardManager.getPrimaryClipDescription(); + // getPrimaryClipDescription can fail if the app does not have input focus + if (description == null) + return false; + + for (int i = 0; i < description.getMimeTypeCount(); ++i) { + String itemMimeType = description.getMimeType(i); + if (itemMimeType.matches(mimeType)) + return true; + } + return false; + } + + @UsedFromNativeCode + public boolean hasClipboardHtml() + { + return hasClipboardMimeType("text/html"); + } + + @UsedFromNativeCode + public String getClipboardHtml() + { + try { + if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { + ClipData primaryClip = m_clipboardManager.getPrimaryClip(); + if (primaryClip != null) { + for (int i = 0; i < primaryClip.getItemCount(); ++i) + if (primaryClip.getItemAt(i).getHtmlText() != null) + return primaryClip.getItemAt(i).getHtmlText(); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get clipboard data", e); + } + return ""; + } + + @UsedFromNativeCode + public void setClipboardUri(Context context, String uriString) + { + if (m_clipboardManager != null) { + ClipData clipData = ClipData.newUri(context.getContentResolver(), "text/uri-list", + Uri.parse(uriString)); + updatePrimaryClip(clipData, context); + } + } + + @UsedFromNativeCode + public boolean hasClipboardUri() + { + return hasClipboardMimeType("text/uri-list"); + } + + @UsedFromNativeCode + private String[] getClipboardUris() + { + ArrayList<String> uris = new ArrayList<>(); + try { + if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { + ClipData primaryClip = m_clipboardManager.getPrimaryClip(); + if (primaryClip != null) { + for (int i = 0; i < primaryClip.getItemCount(); ++i) + if (primaryClip.getItemAt(i).getUri() != null) + uris.add(primaryClip.getItemAt(i).getUri().toString()); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get clipboard data", e); + } + String[] strings = new String[uris.size()]; + strings = uris.toArray(strings); + return strings; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtDisplayManager.java b/src/android/jar/src/org/qtproject/qt/android/QtDisplayManager.java new file mode 100644 index 0000000000..b6a52fb22f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtDisplayManager.java @@ -0,0 +1,287 @@ +// 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 + +package org.qtproject.qt.android; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Size; +import android.view.Display; +import android.view.Surface; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class QtDisplayManager { + + // screen methods + public static native void setDisplayMetrics(int screenWidthPixels, int screenHeightPixels, + int availableLeftPixels, int availableTopPixels, + int availableWidthPixels, int availableHeightPixels, + double XDpi, double YDpi, double scaledDensity, + double density, float refreshRate); + public static native void handleOrientationChanged(int newRotation, int nativeOrientation); + public static native void handleRefreshRateChanged(float refreshRate); + public static native void handleUiDarkModeChanged(int newUiMode); + public static native void handleScreenAdded(int displayId); + public static native void handleScreenChanged(int displayId); + public static native void handleScreenRemoved(int displayId); + // screen methods + + // Keep in sync with QtAndroid::SystemUiVisibility in androidjnimain.h + public static final int SYSTEM_UI_VISIBILITY_NORMAL = 0; + public static final int SYSTEM_UI_VISIBILITY_FULLSCREEN = 1; + public static final int SYSTEM_UI_VISIBILITY_TRANSLUCENT = 2; + private int m_systemUiVisibility = SYSTEM_UI_VISIBILITY_NORMAL; + + private static int m_previousRotation = -1; + + private DisplayManager.DisplayListener m_displayListener = null; + private final Activity m_activity; + + QtDisplayManager(Activity activity) + { + m_activity = activity; + initDisplayListener(); + } + + private void initDisplayListener() { + m_displayListener = new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + QtDisplayManager.handleScreenAdded(displayId); + } + + @Override + public void onDisplayChanged(int displayId) { + Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + ? m_activity.getWindowManager().getDefaultDisplay() + : m_activity.getDisplay(); + float refreshRate = getRefreshRate(display); + QtDisplayManager.handleRefreshRateChanged(refreshRate); + QtDisplayManager.handleScreenChanged(displayId); + } + + @Override + public void onDisplayRemoved(int displayId) { + QtDisplayManager.handleScreenRemoved(displayId); + } + }; + } + + static void handleOrientationChanges(Activity activity) + { + int currentRotation = getDisplayRotation(activity); + if (m_previousRotation == currentRotation) + return; + int nativeOrientation = getNativeOrientation(activity, currentRotation); + QtDisplayManager.handleOrientationChanged(currentRotation, nativeOrientation); + m_previousRotation = currentRotation; + } + + public static int getDisplayRotation(Activity activity) { + Display display = Build.VERSION.SDK_INT < Build.VERSION_CODES.R ? + activity.getWindowManager().getDefaultDisplay() : + activity.getDisplay(); + + return display != null ? display.getRotation() : 0; + } + + private static int getNativeOrientation(Activity activity, int rotation) + { + int orientation = activity.getResources().getConfiguration().orientation; + boolean rot90 = (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270); + boolean isLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE); + if ((isLandscape && !rot90) || (!isLandscape && rot90)) + return Configuration.ORIENTATION_LANDSCAPE; + + return Configuration.ORIENTATION_PORTRAIT; + } + + static float getRefreshRate(Display display) + { + return display != null ? display.getRefreshRate() : 60.0f; + } + + public void registerDisplayListener() + { + DisplayManager displayManager = + (DisplayManager) m_activity.getSystemService(Context.DISPLAY_SERVICE); + displayManager.registerDisplayListener(m_displayListener, null); + } + + public void unregisterDisplayListener() + { + DisplayManager displayManager = + (DisplayManager) m_activity.getSystemService(Context.DISPLAY_SERVICE); + displayManager.unregisterDisplayListener(m_displayListener); + } + + public void setSystemUiVisibility(int systemUiVisibility) + { + if (m_systemUiVisibility == systemUiVisibility) + return; + + m_systemUiVisibility = systemUiVisibility; + + int systemUiVisibilityFlags = View.SYSTEM_UI_FLAG_VISIBLE; + switch (m_systemUiVisibility) { + case SYSTEM_UI_VISIBILITY_NORMAL: + m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + m_activity.getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + } + break; + case SYSTEM_UI_VISIBILITY_FULLSCREEN: + m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + systemUiVisibilityFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.INVISIBLE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + m_activity.getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + } + break; + case SYSTEM_UI_VISIBILITY_TRANSLUCENT: + m_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + m_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + m_activity.getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + } + break; + } + m_activity.getWindow().getDecorView().setSystemUiVisibility(systemUiVisibilityFlags); + } + + public int systemUiVisibility() + { + return m_systemUiVisibility; + } + + public void updateFullScreen() + { + if (m_systemUiVisibility == SYSTEM_UI_VISIBILITY_FULLSCREEN) { + m_systemUiVisibility = SYSTEM_UI_VISIBILITY_NORMAL; + setSystemUiVisibility(SYSTEM_UI_VISIBILITY_FULLSCREEN); + } + } + + @UsedFromNativeCode + public static Display getDisplay(Context context, int displayId) + { + DisplayManager displayManager = + (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); + if (displayManager != null) { + return displayManager.getDisplay(displayId); + } + return null; + } + + @UsedFromNativeCode + public static List<Display> getAvailableDisplays(Context context) + { + DisplayManager displayManager = + (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); + if (displayManager != null) { + Display[] displays = displayManager.getDisplays(); + return Arrays.asList(displays); + } + return new ArrayList<>(); + } + + @UsedFromNativeCode + public static Size getDisplaySize(Context displayContext, Display display) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + return new Size(realMetrics.widthPixels, realMetrics.heightPixels); + } + + Context windowsContext = displayContext.createWindowContext( + WindowManager.LayoutParams.TYPE_APPLICATION, null); + WindowManager windowManager = + (WindowManager) windowsContext.getSystemService(Context.WINDOW_SERVICE); + WindowMetrics windowsMetrics = windowManager.getCurrentWindowMetrics(); + Rect bounds = windowsMetrics.getBounds(); + return new Size(bounds.width(), bounds.height()); + } + + public static void setApplicationDisplayMetrics(Activity activity, int width, int height) + { + if (activity == null) + return; + + final WindowInsets rootInsets = activity.getWindow().getDecorView().getRootWindowInsets(); + final WindowManager windowManager = activity.getWindowManager(); + Display display; + + int insetLeft; + int insetTop; + + int maxWidth; + int maxHeight; + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + display = windowManager.getDefaultDisplay(); + + final DisplayMetrics maxMetrics = new DisplayMetrics(); + display.getRealMetrics(maxMetrics); + maxWidth = maxMetrics.widthPixels; + maxHeight = maxMetrics.heightPixels; + + insetLeft = rootInsets.getStableInsetLeft(); + insetTop = rootInsets.getStableInsetTop(); + } else { + display = activity.getDisplay(); + + final WindowMetrics maxMetrics = windowManager.getMaximumWindowMetrics(); + maxWidth = maxMetrics.getBounds().width(); + maxHeight = maxMetrics.getBounds().height(); + + insetLeft = rootInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()).left; + insetTop = rootInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()).top; + } + + final DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics(); + + double density = displayMetrics.density; + double scaledDensity = displayMetrics.scaledDensity; + + setDisplayMetrics(maxWidth, maxHeight, insetLeft, insetTop, + width, height, getXDpi(displayMetrics), getYDpi(displayMetrics), + scaledDensity, density, getRefreshRate(display)); + } + + public static float getXDpi(final DisplayMetrics metrics) { + if (metrics.xdpi < android.util.DisplayMetrics.DENSITY_LOW) + return android.util.DisplayMetrics.DENSITY_LOW; + return metrics.xdpi; + } + + public static float getYDpi(final DisplayMetrics metrics) { + if (metrics.ydpi < android.util.DisplayMetrics.DENSITY_LOW) + return android.util.DisplayMetrics.DENSITY_LOW; + return metrics.ydpi; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEditText.java b/src/android/jar/src/org/qtproject/qt/android/QtEditText.java index 66d9ccff8b..4524887242 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtEditText.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtEditText.java @@ -5,70 +5,217 @@ package org.qtproject.qt.android; import android.content.Context; +import android.graphics.Canvas; import android.text.InputType; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import android.view.KeyEvent; -public class QtEditText extends View +import org.qtproject.qt.android.QtInputConnection.QtInputConnectionListener; + +class QtEditText extends View { int m_initialCapsMode = 0; int m_imeOptions = 0; int m_inputType = InputType.TYPE_CLASS_TEXT; boolean m_optionsChanged = false; - QtActivityDelegate m_activityDelegate; + QtInputConnection m_inputConnection = null; + + // input method hints - must be kept in sync with QTDIR/src/corelib/global/qnamespace.h + private final int ImhHiddenText = 0x1; + private final int ImhSensitiveData = 0x2; + private final int ImhNoAutoUppercase = 0x4; + private final int ImhPreferNumbers = 0x8; + private final int ImhPreferUppercase = 0x10; + private final int ImhPreferLowercase = 0x20; + private final int ImhNoPredictiveText = 0x40; + + private final int ImhDate = 0x80; + private final int ImhTime = 0x100; + + private final int ImhPreferLatin = 0x200; + + private final int ImhMultiLine = 0x400; + + private final int ImhDigitsOnly = 0x10000; + private final int ImhFormattedNumbersOnly = 0x20000; + private final int ImhUppercaseOnly = 0x40000; + private final int ImhLowercaseOnly = 0x80000; + private final int ImhDialableCharactersOnly = 0x100000; + private final int ImhEmailCharactersOnly = 0x200000; + private final int ImhUrlCharactersOnly = 0x400000; + private final int ImhLatinOnly = 0x800000; + + private final QtInputConnectionListener m_qtInputConnectionListener; + + public QtEditText(Context context, QtInputConnectionListener listener) + { + super(context); + setFocusable(true); + setFocusableInTouchMode(true); + m_qtInputConnectionListener = listener; + } - public void setImeOptions(int m_imeOptions) + private void setImeOptions(int imeOptions) { - if (m_imeOptions == this.m_imeOptions) + if (m_imeOptions == imeOptions) return; - this.m_imeOptions = m_imeOptions; + m_imeOptions = m_imeOptions; m_optionsChanged = true; } - public void setInitialCapsMode(int m_initialCapsMode) + private void setInitialCapsMode(int initialCapsMode) { - if (m_initialCapsMode == this.m_initialCapsMode) + if (m_initialCapsMode == initialCapsMode) return; - this.m_initialCapsMode = m_initialCapsMode; + m_initialCapsMode = initialCapsMode; m_optionsChanged = true; } - public void setInputType(int m_inputType) + private void setInputType(int inputType) { - if (m_inputType == this.m_inputType) + if (m_inputType == inputType) return; - this.m_inputType = m_inputType; + m_inputType = m_inputType; m_optionsChanged = true; } - public QtEditText(Context context, QtActivityDelegate activityDelegate) + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - super(context); - setFocusable(true); - setFocusableInTouchMode(true); - m_activityDelegate = activityDelegate; + outAttrs.inputType = m_inputType; + outAttrs.imeOptions = m_imeOptions; + outAttrs.initialCapsMode = m_initialCapsMode; + m_inputConnection = new QtInputConnection(this,m_qtInputConnectionListener); + return m_inputConnection; } - public QtActivityDelegate getActivityDelegate() + + @Override + public boolean onCheckIsTextEditor () { - return m_activityDelegate; + return true; } @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) + public boolean onKeyDown (int keyCode, KeyEvent event) { - outAttrs.inputType = m_inputType; - outAttrs.imeOptions = m_imeOptions; - outAttrs.initialCapsMode = m_initialCapsMode; - outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; - return new QtInputConnection(this); + if (null != m_inputConnection) + m_inputConnection.restartImmInput(); + + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onDraw(Canvas canvas) { + // DEBUG CODE + // canvas.drawARGB(127, 255, 0, 255); + super.onDraw(canvas); } -// // DEBUG CODE -// @Override -// protected void onDraw(Canvas canvas) { -// canvas.drawARGB(127, 255, 0, 255); -// super.onDraw(canvas); -// } + + public void setEditTextOptions(int enterKeyType, int inputHints) + { + int initialCapsMode = 0; + int imeOptions = imeOptionsFromEnterKeyType(enterKeyType); + int inputType = android.text.InputType.TYPE_CLASS_TEXT; + + if ((inputHints & (ImhPreferNumbers | ImhDigitsOnly | ImhFormattedNumbersOnly)) != 0) { + inputType = android.text.InputType.TYPE_CLASS_NUMBER; + if ((inputHints & ImhFormattedNumbersOnly) != 0) { + inputType |= (android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL + | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED); + } + + if ((inputHints & ImhHiddenText) != 0) + inputType |= android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD; + } else if ((inputHints & ImhDialableCharactersOnly) != 0) { + inputType = android.text.InputType.TYPE_CLASS_PHONE; + } else if ((inputHints & (ImhDate | ImhTime)) != 0) { + inputType = android.text.InputType.TYPE_CLASS_DATETIME; + if ((inputHints & (ImhDate | ImhTime)) != (ImhDate | ImhTime)) { + if ((inputHints & ImhDate) != 0) + inputType |= android.text.InputType.TYPE_DATETIME_VARIATION_DATE; + else + inputType |= android.text.InputType.TYPE_DATETIME_VARIATION_TIME; + } // else { TYPE_DATETIME_VARIATION_NORMAL(0) } + } else { // CLASS_TEXT + if ((inputHints & ImhHiddenText) != 0) { + inputType |= android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD; + } else if ((inputHints & ImhSensitiveData) != 0 || + isDisablePredictiveTextWorkaround(inputHints)) { + inputType |= android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; + } else if ((inputHints & ImhUrlCharactersOnly) != 0) { + inputType |= android.text.InputType.TYPE_TEXT_VARIATION_URI; + if (enterKeyType == 0) // not explicitly overridden + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_GO; + } else if ((inputHints & ImhEmailCharactersOnly) != 0) { + inputType |= android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + } + + if ((inputHints & ImhMultiLine) != 0) { + inputType |= android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE; + // Clear imeOptions for Multi-Line Type + // User should be able to insert new line in such case + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE; + } + if ((inputHints & (ImhNoPredictiveText | ImhSensitiveData | ImhHiddenText)) != 0) + inputType |= android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + + if ((inputHints & ImhUppercaseOnly) != 0) { + initialCapsMode |= android.text.TextUtils.CAP_MODE_CHARACTERS; + inputType |= android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; + } else if ((inputHints & ImhLowercaseOnly) == 0 + && (inputHints & ImhNoAutoUppercase) == 0) { + initialCapsMode |= android.text.TextUtils.CAP_MODE_SENTENCES; + inputType |= android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; + } + } + + if (enterKeyType == 0 && (inputHints & ImhMultiLine) != 0) + imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION; + + setInitialCapsMode(initialCapsMode); + setImeOptions(imeOptions); + setInputType(inputType); + } + + private int imeOptionsFromEnterKeyType(int enterKeyType) + { + int imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE; + + // enter key type - must be kept in sync with QTDIR/src/corelib/global/qnamespace.h + switch (enterKeyType) { + case 0: // EnterKeyDefault + break; + case 1: // EnterKeyReturn + imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION; + break; + case 2: // EnterKeyDone + break; + case 3: // EnterKeyGo + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_GO; + break; + case 4: // EnterKeySend + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEND; + break; + case 5: // EnterKeySearch + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH; + break; + case 6: // EnterKeyNext + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_NEXT; + break; + case 7: // EnterKeyPrevious + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS; + break; + } + return imeOptions; + } + + private boolean isDisablePredictiveTextWorkaround(int inputHints) + { + return (inputHints & ImhNoPredictiveText) != 0 && + System.getenv("QT_ANDROID_ENABLE_WORKAROUND_TO_DISABLE_PREDICTIVE_TEXT") != null; + } } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java new file mode 100644 index 0000000000..5298ac02bd --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java @@ -0,0 +1,217 @@ +// 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 static org.qtproject.qt.android.QtNative.ApplicationState.*; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; + +import java.util.ArrayList; +import java.util.HashMap; + +class QtEmbeddedDelegate extends QtActivityDelegateBase + implements QtNative.AppStateDetailsListener, QtEmbeddedViewInterface, QtWindowInterface, + QtMenuInterface, QtLayoutInterface +{ + private static final String QtTAG = "QtEmbeddedDelegate"; + // TODO simplistic implementation with one QtView, expand to support multiple views QTBUG-117649 + private QtView m_view; + private QtNative.ApplicationStateDetails m_stateDetails; + private boolean m_windowLoaded = false; + private boolean m_backendsRegistered = false; + + public QtEmbeddedDelegate(Activity context) { + super(context); + + m_stateDetails = QtNative.getStateDetails(); + QtNative.registerAppStateListener(this); + + m_activity.getApplication().registerActivityLifecycleCallbacks( + new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) { + if (m_activity == activity && m_stateDetails.isStarted) { + QtNative.setApplicationState(ApplicationActive); + QtNative.updateWindow(); + } + } + + @Override + public void onActivityPaused(Activity activity) { + if (m_activity == activity && m_stateDetails.isStarted) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || + !activity.isInMultiWindowMode()) { + QtNative.setApplicationState(ApplicationInactive); + } + } + } + + @Override + public void onActivityStopped(Activity activity) { + if (m_activity == activity && m_stateDetails.isStarted) { + QtNative.setApplicationState(ApplicationSuspended); + } + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public void onActivityDestroyed(Activity activity) { + if (m_activity == activity && m_stateDetails.isStarted) { + m_activity.getApplication().unregisterActivityLifecycleCallbacks(this); + QtNative.unregisterAppStateListener(QtEmbeddedDelegate.this); + QtEmbeddedViewInterfaceFactory.remove(m_activity); + QtNative.terminateQt(); + QtNative.setActivity(null); + QtNative.getQtThread().exit(); + } + } + }); + } + + @Override + public void onAppStateDetailsChanged(QtNative.ApplicationStateDetails details) { + synchronized (this) { + m_stateDetails = details; + if (details.isStarted && !m_backendsRegistered) { + m_backendsRegistered = true; + BackendRegister.registerBackend(QtWindowInterface.class, (QtWindowInterface)this); + BackendRegister.registerBackend(QtMenuInterface.class, (QtMenuInterface)this); + BackendRegister.registerBackend(QtLayoutInterface.class, (QtLayoutInterface)this); + } else if (!details.isStarted && m_backendsRegistered) { + m_backendsRegistered = false; + BackendRegister.unregisterBackend(QtWindowInterface.class); + BackendRegister.unregisterBackend(QtMenuInterface.class); + BackendRegister.unregisterBackend(QtLayoutInterface.class); + } + } + } + + @Override + public void onNativePluginIntegrationReadyChanged(boolean ready) + { + synchronized (this) { + if (ready) { + QtNative.runAction(() -> { + DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + QtDisplayManager.setApplicationDisplayMetrics(m_activity, metrics.widthPixels, + metrics.heightPixels); + + }); + createRootWindow(); + } + } + } + + @Override + void startNativeApplicationImpl(String appParams, String mainLib) + { + QtNative.startApplication(appParams, mainLib); + } + + @Override + public QtLayout getQtLayout() + { + // 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 + if (m_view == null) + return null; + return m_view.getQtWindow(); + } + + // QtEmbeddedViewInterface implementation begin + @Override + public void startQtApplication(String appParams, String mainLib) + { + super.startNativeApplication(appParams, mainLib); + } + + @Override + public void queueLoadWindow() + { + synchronized (this) { + if (m_stateDetails.nativePluginIntegrationReady) + createRootWindow(); + } + } + + @Override + public void setView(QtView view) + { + m_view = view; + updateInputDelegate(); + if (m_view != null) + registerGlobalFocusChangeListener(m_view); + } + // QtEmbeddedViewInterface implementation end + + private void updateInputDelegate() { + if (m_view == null) { + m_inputDelegate.setEditPopupMenu(null); + return; + } + m_inputDelegate.setEditPopupMenu(new EditPopupMenu(m_activity, m_view)); + } + + private void createRootWindow() { + if (m_view != null && !m_windowLoaded) { + QtView.createRootWindow(m_view, m_view.getLeft(), m_view.getTop(), m_view.getWidth(), + m_view.getHeight()); + m_windowLoaded = true; + } + } + + // QtMenuInterface implementation begin + @Override + public void resetOptionsMenu() { QtNative.runAction(() -> m_activity.invalidateOptionsMenu()); } + + @Override + public void openOptionsMenu() { QtNative.runAction(() -> m_activity.openOptionsMenu()); } + + @Override + public void closeContextMenu() { QtNative.runAction(() -> m_activity.closeContextMenu()); } + + @Override + public void openContextMenu(final int x, final int y, final int w, final int h) + { + QtLayout layout = getQtLayout(); + layout.postDelayed(() -> { + final QtEditText focusedEditText = m_inputDelegate.getCurrentQtEditText(); + if (focusedEditText == null) { + Log.w(QtTAG, "No focused view when trying to open context menu"); + return; + } + layout.setLayoutParams(focusedEditText, new QtLayout.LayoutParams(w, h, x, y), false); + PopupMenu popup = new PopupMenu(m_activity, focusedEditText); + QtNative.fillContextMenu(popup.getMenu()); + popup.setOnMenuItemClickListener(menuItem -> + m_activity.onContextItemSelected(menuItem)); + popup.setOnDismissListener(popupMenu -> + m_activity.onContextMenuClosed(popupMenu.getMenu())); + popup.show(); + }, 100); + } + // QtMenuInterface implementation end +} 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..69ecced7ff --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java @@ -0,0 +1,53 @@ +// 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 + int displayDensity = m_context.getResources().getDisplayMetrics().densityDpi; + setEnvironmentVariable("QT_ANDROID_THEME_DISPLAY_DPI", String.valueOf(displayDensity)); + String stylePath = ExtractStyle.setup(m_context, "minimal", displayDensity); + setEnvironmentVariable("ANDROID_STYLE_PATH", stylePath); + setEnvironmentVariable("QT_ANDROID_NO_EXIT_CALL", String.valueOf(true)); + } + + @Override + protected void finish() { + // Called when loading fails - clear the delegate to make sure we don't hold reference + // to the embedding Context + QtEmbeddedViewInterfaceFactory.remove((Activity)m_context.getBaseContext()); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterface.java new file mode 100644 index 0000000000..a83a65e32c --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterface.java @@ -0,0 +1,15 @@ +// 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; + +/** + * QtEmbeddedViewInterface is intended to encapsulate the needs of QtView, so that the Activity and + * Service implementations of these functions may be split clearly, and the interface can be stored + * and used conveniently in QtView. +**/ +interface QtEmbeddedViewInterface { + void startQtApplication(String appParams, String mainLib); + void setView(QtView view); + void queueLoadWindow(); +}; diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterfaceFactory.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterfaceFactory.java new file mode 100644 index 0000000000..8a5764e93f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedViewInterfaceFactory.java @@ -0,0 +1,34 @@ +// 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.content.Context; +import android.app.Activity; +import android.app.Service; + +import java.util.HashMap; + +class QtEmbeddedViewInterfaceFactory { + private static final HashMap<Context, QtEmbeddedViewInterface> m_interfaces = new HashMap<>(); + private static final Object m_interfaceLock = new Object(); + + public static QtEmbeddedViewInterface create(Context context) { + synchronized (m_interfaceLock) { + if (!m_interfaces.containsKey(context)) { + if (context instanceof Activity) + m_interfaces.put(context, new QtEmbeddedDelegate((Activity)context)); + else if (context instanceof Service) + m_interfaces.put(context, new QtServiceEmbeddedDelegate((Service)context)); + } + + return m_interfaces.get(context); + } + } + + public static void remove(Context context) { + synchronized (m_interfaceLock) { + m_interfaces.remove(context); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java b/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java index abcc76da17..b95f817d33 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java @@ -6,6 +6,7 @@ package org.qtproject.qt.android; import android.content.Context; import android.os.Build; +import android.util.Log; import android.view.WindowMetrics; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; @@ -49,34 +50,11 @@ class QtNativeInputConnection static native boolean copyURL(); static native boolean paste(); static native boolean updateCursorPosition(); + static native void reportFullscreenMode(boolean enabled); + static native boolean fullscreenMode(); } -class HideKeyboardRunnable implements Runnable { - private long m_hideTimeStamp = System.nanoTime(); - - @Override - public void run() { - // Check that the keyboard is really no longer there. - Activity activity = QtNative.activity(); - Rect r = new Rect(); - activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); - - int screenHeight = 0; - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - DisplayMetrics metrics = new DisplayMetrics(); - activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - screenHeight = metrics.heightPixels; - } else { - final WindowMetrics maximumWindowMetrics = activity.getWindowManager().getMaximumWindowMetrics(); - screenHeight = maximumWindowMetrics.getBounds().height(); - } - final int kbHeight = screenHeight - r.bottom; - if (kbHeight < 100) - QtNative.activityDelegate().setKeyboardVisibility(false, m_hideTimeStamp); - } -} - -public class QtInputConnection extends BaseInputConnection +class QtInputConnection extends BaseInputConnection { private static final int ID_SELECT_ALL = android.R.id.selectAll; private static final int ID_CUT = android.R.id.cut; @@ -86,21 +64,75 @@ public class QtInputConnection extends BaseInputConnection private static final int ID_SWITCH_INPUT_METHOD = android.R.id.switchInputMethod; private static final int ID_ADD_TO_DICTIONARY = android.R.id.addToDictionary; - private QtEditText m_view = null; + private static final String QtTAG = "QtInputConnection"; + + private final QtInputConnectionListener m_qtInputConnectionListener; + + class HideKeyboardRunnable implements Runnable { + @Override + public void run() { + // Check that the keyboard is really no longer there. + Activity activity = QtNative.activity(); + if (activity == null) { + Log.w(QtTAG, "HideKeyboardRunnable: The activity reference is null"); + return; + } + if (m_qtInputConnectionListener == null) { + Log.w(QtTAG, "HideKeyboardRunnable: QtInputConnectionListener is null"); + return; + } + + Rect r = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); + + int screenHeight; + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + screenHeight = metrics.heightPixels; + } else { + final WindowMetrics maximumWindowMetrics = activity.getWindowManager().getMaximumWindowMetrics(); + screenHeight = maximumWindowMetrics.getBounds().height(); + } + final int kbHeight = screenHeight - r.bottom; + if (kbHeight < 100) + m_qtInputConnectionListener.onHideKeyboardRunnableDone(false, System.nanoTime()); + } + } + + public interface QtInputConnectionListener { + void onSetClosing(boolean closing); + void onHideKeyboardRunnableDone(boolean visibility, long hideTimeStamp); + void onSendKeyEventDefaultCase(); + } + + private final QtEditText m_view; + private final InputMethodManager m_imm; private void setClosing(boolean closing) { - if (closing) { + if (closing) m_view.postDelayed(new HideKeyboardRunnable(), 100); - } else { - QtNative.activityDelegate().setKeyboardVisibility(true, System.nanoTime()); - } + else if (m_qtInputConnectionListener != null) + m_qtInputConnectionListener.onSetClosing(false); } - public QtInputConnection(QtEditText targetView) + public QtInputConnection(QtEditText targetView, QtInputConnectionListener listener) { super(targetView, true); m_view = targetView; + m_imm = (InputMethodManager)m_view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + m_qtInputConnectionListener = listener; + } + + public void restartImmInput() + { + if (QtNativeInputConnection.fullscreenMode()) { + if (m_imm != null) + m_imm.restartInput(m_view); + } + } @Override @@ -111,6 +143,18 @@ public class QtInputConnection extends BaseInputConnection } @Override + public boolean reportFullscreenMode (boolean enabled) + { + QtNativeInputConnection.reportFullscreenMode(enabled); + // Always ignored on calling editor. + // Always false on Android 8 and later, true with earlier. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + return false; + + return true; + } + + @Override public boolean endBatchEdit() { setClosing(false); @@ -128,6 +172,7 @@ public class QtInputConnection extends BaseInputConnection public boolean commitText(CharSequence text, int newCursorPosition) { setClosing(false); + restartImmInput(); return QtNativeInputConnection.commitText(text.toString(), newCursorPosition); } @@ -193,23 +238,25 @@ public class QtInputConnection extends BaseInputConnection { switch (id) { case ID_SELECT_ALL: + restartImmInput(); return QtNativeInputConnection.selectAll(); case ID_COPY: + restartImmInput(); return QtNativeInputConnection.copy(); case ID_COPY_URL: + restartImmInput(); return QtNativeInputConnection.copyURL(); case ID_CUT: + restartImmInput(); return QtNativeInputConnection.cut(); case ID_PASTE: + restartImmInput(); return QtNativeInputConnection.paste(); - case ID_SWITCH_INPUT_METHOD: - InputMethodManager imm = (InputMethodManager)m_view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) - imm.showInputMethodPicker(); + if (m_imm != null) + m_imm.showInputMethodPicker(); return true; - case ID_ADD_TO_DICTIONARY: // TODO // String word = m_editable.subSequence(0, m_editable.length()).toString(); @@ -242,8 +289,7 @@ public class QtInputConnection extends BaseInputConnection event.getRepeatCount(), event.getMetaState()); return super.sendKeyEvent(fakeEvent); - - case android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS: + case android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS: fakeEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), @@ -251,16 +297,15 @@ public class QtInputConnection extends BaseInputConnection event.getRepeatCount(), KeyEvent.META_SHIFT_ON); return super.sendKeyEvent(fakeEvent); - case android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION: + restartImmInput(); break; - default: - QtNative.activityDelegate().hideSoftwareKeyboard(); - break; + if (m_qtInputConnectionListener != null) + m_qtInputConnectionListener.onSendKeyEventDefaultCase(); + break; } } - return super.sendKeyEvent(event); } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java new file mode 100644 index 0000000000..bf5578285a --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java @@ -0,0 +1,665 @@ +// 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 + +package org.qtproject.qt.android; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.text.method.MetaKeyKeyListener; +import android.util.DisplayMetrics; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +import org.qtproject.qt.android.QtInputConnection.QtInputConnectionListener; + +/** @noinspection FieldCanBeLocal*/ +class QtInputDelegate implements QtInputConnection.QtInputConnectionListener, QtInputInterface +{ + + // keyboard methods + public static native void keyDown(int key, int unicode, int modifier, boolean autoRepeat); + public static native void keyUp(int key, int unicode, int modifier, boolean autoRepeat); + public static native void keyboardVisibilityChanged(boolean visibility); + public static native void keyboardGeometryChanged(int x, int y, int width, int height); + // keyboard methods + + // dispatch events methods + public static native boolean dispatchGenericMotionEvent(MotionEvent event); + public static native boolean dispatchKeyEvent(KeyEvent event); + // dispatch events methods + + // handle methods + public static native void handleLocationChanged(int id, int x, int y); + // handle methods + + private QtEditText m_currentEditText = null; + private final InputMethodManager m_imm; + + private boolean m_keyboardIsVisible = false; + private boolean m_isKeyboardHidingAnimationOngoing = false; + private long m_showHideTimeStamp = System.nanoTime(); + private int m_portraitKeyboardHeight = 0; + private int m_landscapeKeyboardHeight = 0; + private int m_probeKeyboardHeightDelayMs = 50; + private CursorHandle m_cursorHandle; + private CursorHandle m_leftSelectionHandle; + private CursorHandle m_rightSelectionHandle; + private EditPopupMenu m_editPopupMenu; + + private int m_softInputMode = 0; + + // Values coming from QAndroidInputContext::CursorHandleShowMode + private static final int CursorHandleNotShown = 0; + private static final int CursorHandleShowNormal = 1; + private static final int CursorHandleShowSelection = 2; + private static final int CursorHandleShowEdit = 0x100; + + // Handle IDs + public static final int IdCursorHandle = 1; + public static final int IdLeftHandle = 2; + public static final int IdRightHandle = 3; + + private static Boolean m_tabletEventSupported = null; + + private static int m_oldX, m_oldY; + + + private long m_metaState; + private int m_lastChar = 0; + private boolean m_backKeyPressedSent = false; + + // Note: because of the circular call to updateFullScreen() from the delegate, we need + // a listener to be able to do that call from the delegate, because that's where that + // logic lives + public interface KeyboardVisibilityListener { + void onKeyboardVisibilityChange(); + } + + private final KeyboardVisibilityListener m_keyboardVisibilityListener; + + QtInputDelegate(Activity activity, KeyboardVisibilityListener listener) + { + this.m_keyboardVisibilityListener = listener; + m_imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + // QtInputInterface implementation begin + @Override + public void updateSelection(final int selStart, final int selEnd, + final int candidatesStart, final int candidatesEnd) + { + QtNative.runAction(() -> { + if (m_imm == null) + return; + + m_imm.updateSelection(m_currentEditText, selStart, selEnd, candidatesStart, candidatesEnd); + }); + } + + @Override + public void showSoftwareKeyboard(Activity activity, QtLayout layout, + final int x, final int y, final int width, final int height, + final int inputHints, final int enterKeyType) + { + QtNative.runAction(() -> { + if (m_imm == null || m_currentEditText == null) + return; + + if (updateSoftInputMode(activity, height)) + return; + + m_currentEditText.setEditTextOptions(enterKeyType, inputHints); + + m_currentEditText.postDelayed(() -> { + m_imm.showSoftInput(m_currentEditText, 0, new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case InputMethodManager.RESULT_SHOWN: + QtNativeInputConnection.updateCursorPosition(); + //FALLTHROUGH + case InputMethodManager.RESULT_UNCHANGED_SHOWN: + setKeyboardVisibility(true, System.nanoTime()); + if (m_softInputMode == 0) { + probeForKeyboardHeight(layout, activity, + x, y, width, height, inputHints, enterKeyType); + } + break; + case InputMethodManager.RESULT_HIDDEN: + case InputMethodManager.RESULT_UNCHANGED_HIDDEN: + setKeyboardVisibility(false, System.nanoTime()); + break; + } + } + }); + if (m_currentEditText.m_optionsChanged) { + m_imm.restartInput(m_currentEditText); + m_currentEditText.m_optionsChanged = false; + } + }, 15); + }); + } + + @Override + public int getSelectHandleWidth() + { + int width = 0; + if (m_leftSelectionHandle != null && m_rightSelectionHandle != null) { + width = Math.max(m_leftSelectionHandle.width(), m_rightSelectionHandle.width()); + } else if (m_cursorHandle != null) { + width = m_cursorHandle.width(); + } + return width; + } + + /* called from the C++ code when the position of the cursor or selection handles needs to + be adjusted. + mode is one of QAndroidInputContext::CursorHandleShowMode + */ + @Override + public void updateHandles(Activity activity, QtLayout layout, int mode, + int editX, int editY, int editButtons, + int x1, int y1, int x2, int y2, boolean rtl) + { + QtNative.runAction(() -> updateHandleImpl(activity, layout, mode, editX, editY, editButtons, + x1, y1, x2, y2, rtl)); + } + + @Override + public QtInputConnection.QtInputConnectionListener getInputConnectionListener() + { + return this; + } + + @Override + public void resetSoftwareKeyboard() + { + if (m_imm == null || m_currentEditText == null) + return; + m_currentEditText.postDelayed(() -> { + m_imm.restartInput(m_currentEditText); + m_currentEditText.m_optionsChanged = false; + }, 5); + } + + @Override + public void hideSoftwareKeyboard() + { + m_isKeyboardHidingAnimationOngoing = true; + QtNative.runAction(() -> { + if (m_imm == null || m_currentEditText == null) + return; + + m_imm.hideSoftInputFromWindow(m_currentEditText.getWindowToken(), 0, + new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case InputMethodManager.RESULT_SHOWN: + case InputMethodManager.RESULT_UNCHANGED_SHOWN: + setKeyboardVisibility(true, System.nanoTime()); + break; + case InputMethodManager.RESULT_HIDDEN: + case InputMethodManager.RESULT_UNCHANGED_HIDDEN: + setKeyboardVisibility(false, System.nanoTime()); + break; + } + } + }); + }); + } + + // Is the keyboard fully visible i.e. visible and no ongoing animation + @Override + public boolean isSoftwareKeyboardVisible() + { + return isKeyboardVisible() && !m_isKeyboardHidingAnimationOngoing; + } + // QtInputInterface implementation end + + // QtInputConnectionListener methods + @Override + public void onSetClosing(boolean closing) { + if (!closing) + setKeyboardVisibility(true, System.nanoTime()); + } + + @Override + public void onHideKeyboardRunnableDone(boolean visibility, long hideTimeStamp) { + setKeyboardVisibility(visibility, hideTimeStamp); + } + + @Override + public void onSendKeyEventDefaultCase() { + hideSoftwareKeyboard(); + } + // QtInputConnectionListener methods + + public boolean isKeyboardVisible() + { + return m_keyboardIsVisible; + } + + void setSoftInputMode(int inputMode) + { + m_softInputMode = inputMode; + } + + QtEditText getCurrentQtEditText() + { + return m_currentEditText; + } + + void setEditPopupMenu(EditPopupMenu editPopupMenu) + { + m_editPopupMenu = editPopupMenu; + } + + private void keyboardVisibilityUpdated(boolean visibility) + { + m_isKeyboardHidingAnimationOngoing = false; + QtInputDelegate.keyboardVisibilityChanged(visibility); + } + + public void setKeyboardVisibility(boolean visibility, long timeStamp) + { + if (m_showHideTimeStamp > timeStamp) + return; + m_showHideTimeStamp = timeStamp; + + if (m_keyboardIsVisible == visibility) + return; + m_keyboardIsVisible = visibility; + keyboardVisibilityUpdated(m_keyboardIsVisible); + + // Hiding the keyboard clears the immersive mode, so we need to set it again. + if (!visibility) + m_keyboardVisibilityListener.onKeyboardVisibilityChange(); + + } + + void setFocusedView(QtEditText currentEditText) + { + m_currentEditText = currentEditText; + } + + private boolean updateSoftInputMode(Activity activity, int height) + { + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + // If the screen is in portrait mode than we estimate that keyboard height + // will not be higher than 2/5 of the screen. Otherwise we estimate that keyboard height + // will not be higher than 2/3 of the screen + final int visibleHeight; + if (metrics.widthPixels < metrics.heightPixels) { + visibleHeight = m_portraitKeyboardHeight != 0 ? + m_portraitKeyboardHeight : metrics.heightPixels * 3 / 5; + } else { + visibleHeight = m_landscapeKeyboardHeight != 0 ? + m_landscapeKeyboardHeight : metrics.heightPixels / 3; + } + + if (m_softInputMode != 0) { + activity.getWindow().setSoftInputMode(m_softInputMode); + int stateHidden = WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; + return (m_softInputMode & stateHidden) != 0; + } else { + int stateUnchanged = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; + if (height > visibleHeight) { + int adjustResize = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + activity.getWindow().setSoftInputMode(stateUnchanged | adjustResize); + } else { + int adjustPan = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; + activity.getWindow().setSoftInputMode(stateUnchanged | adjustPan); + } + } + return false; + } + + private void probeForKeyboardHeight(QtLayout layout, Activity activity, int x, int y, + int width, int height, int inputHints, int enterKeyType) + { + layout.postDelayed(() -> { + if (!m_keyboardIsVisible) + return; + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + Rect r = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); + if (metrics.heightPixels != r.bottom) { + if (metrics.widthPixels > metrics.heightPixels) { // landscape + if (m_landscapeKeyboardHeight != r.bottom) { + m_landscapeKeyboardHeight = r.bottom; + showSoftwareKeyboard(activity, layout, x, y, width, height, + inputHints, enterKeyType); + } + } else { + if (m_portraitKeyboardHeight != r.bottom) { + m_portraitKeyboardHeight = r.bottom; + showSoftwareKeyboard(activity, layout, x, y, width, height, + inputHints, enterKeyType); + } + } + } else { + // no luck ? + // maybe the delay was too short, so let's make it longer + if (m_probeKeyboardHeightDelayMs < 1000) + m_probeKeyboardHeightDelayMs *= 2; + } + }, m_probeKeyboardHeightDelayMs); + } + + private void updateHandleImpl(Activity activity, QtLayout layout, int mode, + int editX, int editY, int editButtons, + int x1, int y1, int x2, int y2, boolean rtl) + { + switch (mode & 0xff) + { + case CursorHandleNotShown: + if (m_cursorHandle != null) { + m_cursorHandle.hide(); + m_cursorHandle = null; + } + if (m_rightSelectionHandle != null) { + m_rightSelectionHandle.hide(); + m_leftSelectionHandle.hide(); + m_rightSelectionHandle = null; + m_leftSelectionHandle = null; + } + if (m_editPopupMenu != null) + m_editPopupMenu.hide(); + break; + + case CursorHandleShowNormal: + if (m_cursorHandle == null) { + m_cursorHandle = new CursorHandle(activity, layout, IdCursorHandle, + android.R.attr.textSelectHandle, false); + } + m_cursorHandle.setPosition(x1, y1); + if (m_rightSelectionHandle != null) { + m_rightSelectionHandle.hide(); + m_leftSelectionHandle.hide(); + m_rightSelectionHandle = null; + m_leftSelectionHandle = null; + } + break; + + case CursorHandleShowSelection: + if (m_rightSelectionHandle == null) { + m_leftSelectionHandle = new CursorHandle(activity, layout, IdLeftHandle, + !rtl ? android.R.attr.textSelectHandleLeft : + android.R.attr.textSelectHandleRight, + rtl); + m_rightSelectionHandle = new CursorHandle(activity, layout, IdRightHandle, + !rtl ? android.R.attr.textSelectHandleRight : + android.R.attr.textSelectHandleLeft, + rtl); + } + m_leftSelectionHandle.setPosition(x1,y1); + m_rightSelectionHandle.setPosition(x2,y2); + if (m_cursorHandle != null) { + m_cursorHandle.hide(); + m_cursorHandle = null; + } + mode |= CursorHandleShowEdit; + break; + } + + if (!QtClipboardManager.hasClipboardText(activity)) + editButtons &= ~EditContextView.PASTE_BUTTON; + + if (m_editPopupMenu != null) { + if ((mode & CursorHandleShowEdit) == CursorHandleShowEdit && editButtons != 0) { + m_editPopupMenu.setPosition(editX, editY, editButtons, + m_cursorHandle, m_leftSelectionHandle, m_rightSelectionHandle); + } else { + m_editPopupMenu.hide(); + } + } + } + + public boolean onKeyDown(int keyCode, KeyEvent event) + { + m_metaState = MetaKeyKeyListener.handleKeyDown(m_metaState, keyCode, event); + int metaState = MetaKeyKeyListener.getMetaState(m_metaState) | event.getMetaState(); + int c = event.getUnicodeChar(metaState); + int lc = c; + m_metaState = MetaKeyKeyListener.adjustMetaAfterKeypress(m_metaState); + + if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { + c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; + c = KeyEvent.getDeadChar(m_lastChar, c); + } + + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_MUTE) + && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { + return false; + } + + m_lastChar = lc; + if (keyCode == KeyEvent.KEYCODE_BACK) { + m_backKeyPressedSent = !isKeyboardVisible(); + if (!m_backKeyPressedSent) + return true; + } + + QtInputDelegate.keyDown(keyCode, c, event.getMetaState(), event.getRepeatCount() > 0); + + return true; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) + { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_MUTE) + && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { + return false; + } + + if (keyCode == KeyEvent.KEYCODE_BACK && !m_backKeyPressedSent) { + hideSoftwareKeyboard(); + setKeyboardVisibility(false, System.nanoTime()); + return true; + } + + m_metaState = MetaKeyKeyListener.handleKeyUp(m_metaState, keyCode, event); + boolean autoRepeat = event.getRepeatCount() > 0; + QtInputDelegate.keyUp(keyCode, event.getUnicodeChar(), event.getMetaState(), autoRepeat); + + return true; + } + + public boolean handleDispatchKeyEvent(KeyEvent event) + { + if (event.getAction() == KeyEvent.ACTION_MULTIPLE + && event.getCharacters() != null + && event.getCharacters().length() == 1 + && event.getKeyCode() == 0) { + keyDown(0, event.getCharacters().charAt(0), event.getMetaState(), + event.getRepeatCount() > 0); + keyUp(0, event.getCharacters().charAt(0), event.getMetaState(), + event.getRepeatCount() > 0); + } + + return dispatchKeyEvent(event); + } + + public boolean handleDispatchGenericMotionEvent(MotionEvent event) + { + return dispatchGenericMotionEvent(event); + } + + ////////////////////////////// + // Mouse and Touch Input // + ////////////////////////////// + + // tablet methods + public static native boolean isTabletEventSupported(); + public static native void tabletEvent(int winId, int deviceId, long time, int action, + int pointerType, int buttonState, float x, float y, + float pressure); + // tablet methods + + // pointer methods + public static native void mouseDown(int winId, int x, int y, int mouseButtonState); + public static native void mouseUp(int winId, int x, int y, int mouseButtonState); + public static native void mouseMove(int winId, int x, int y); + public static native void mouseWheel(int winId, int x, int y, float hDelta, float vDelta); + public static native void touchBegin(int winId); + public static native void touchAdd(int winId, int pointerId, int action, boolean primary, + int x, int y, float major, float minor, float rotation, + float pressure); + public static native void touchEnd(int winId, int action); + public static native void touchCancel(int winId); + public static native void longPress(int winId, int x, int y); + // pointer methods + + static private int getAction(int index, MotionEvent event) + { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_MOVE) { + int hsz = event.getHistorySize(); + if (hsz > 0) { + float x = event.getX(index); + float y = event.getY(index); + for (int h = 0; h < hsz; ++h) { + if ( event.getHistoricalX(index, h) != x || + event.getHistoricalY(index, h) != y ) + return 1; + } + return 2; + } + return 1; + } + if (action == MotionEvent.ACTION_DOWN + || action == MotionEvent.ACTION_POINTER_DOWN && index == event.getActionIndex()) { + return 0; + } else if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_POINTER_UP && index == event.getActionIndex()) { + return 3; + } + return 2; + } + + static public void sendTouchEvent(MotionEvent event, int id) + { + int pointerType = 0; + + if (m_tabletEventSupported == null) + m_tabletEventSupported = isTabletEventSupported(); + + switch (event.getToolType(0)) { + case MotionEvent.TOOL_TYPE_STYLUS: + pointerType = 1; // QTabletEvent::Pen + break; + case MotionEvent.TOOL_TYPE_ERASER: + pointerType = 3; // QTabletEvent::Eraser + break; + } + + if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + sendMouseEvent(event, id); + } else if (m_tabletEventSupported && pointerType != 0) { + tabletEvent(id, event.getDeviceId(), event.getEventTime(), event.getActionMasked(), + pointerType, event.getButtonState(), + event.getX(), event.getY(), event.getPressure()); + } else { + touchBegin(id); + for (int i = 0; i < event.getPointerCount(); ++i) { + touchAdd(id, + event.getPointerId(i), + getAction(i, event), + i == 0, + (int)event.getX(i), + (int)event.getY(i), + event.getTouchMajor(i), + event.getTouchMinor(i), + event.getOrientation(i), + event.getPressure(i)); + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchEnd(id, 0); + break; + + case MotionEvent.ACTION_UP: + touchEnd(id, 2); + break; + + case MotionEvent.ACTION_CANCEL: + touchCancel(id); + break; + + default: + touchEnd(id, 1); + } + } + } + + static public void sendTrackballEvent(MotionEvent event, int id) + { + sendMouseEvent(event,id); + } + + static public boolean sendGenericMotionEvent(MotionEvent event, int id) + { + int scrollOrHoverMove = MotionEvent.ACTION_SCROLL | MotionEvent.ACTION_HOVER_MOVE; + int pointerDeviceModifier = (event.getSource() & InputDevice.SOURCE_CLASS_POINTER); + boolean isPointerDevice = pointerDeviceModifier == InputDevice.SOURCE_CLASS_POINTER; + + if ((event.getAction() & scrollOrHoverMove) == 0 || !isPointerDevice ) + return false; + + return sendMouseEvent(event, id); + } + + static public boolean sendMouseEvent(MotionEvent event, int id) + { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + mouseUp(id, (int) event.getX(), (int) event.getY(), event.getButtonState()); + break; + + case MotionEvent.ACTION_DOWN: + mouseDown(id, (int) event.getX(), (int) event.getY(), event.getButtonState()); + m_oldX = (int) event.getX(); + m_oldY = (int) event.getY(); + break; + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_MOVE: + if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + mouseMove(id, (int) event.getX(), (int) event.getY()); + } else { + int dx = (int) (event.getX() - m_oldX); + int dy = (int) (event.getY() - m_oldY); + if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { + mouseMove(id, (int) event.getX(), (int) event.getY()); + m_oldX = (int) event.getX(); + m_oldY = (int) event.getY(); + } + } + break; + case MotionEvent.ACTION_SCROLL: + mouseWheel(id, (int) event.getX(), (int) event.getY(), + event.getAxisValue(MotionEvent.AXIS_HSCROLL), + event.getAxisValue(MotionEvent.AXIS_VSCROLL)); + break; + default: + return false; + } + return true; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtInputInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtInputInterface.java new file mode 100644 index 0000000000..1dc4d5fd7f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputInterface.java @@ -0,0 +1,21 @@ +// 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; + +@UsedFromNativeCode +interface QtInputInterface { + void updateSelection(final int selStart, final int selEnd, final int candidatesStart, + final int candidatesEnd); + void showSoftwareKeyboard(Activity activity, QtLayout layout, final int x, final int y, + final int width, final int height, final int inputHints, + final int enterKeyType); + void resetSoftwareKeyboard(); + void hideSoftwareKeyboard(); + boolean isSoftwareKeyboardVisible(); + int getSelectHandleWidth(); + void updateHandles(Activity activity, QtLayout layout, int mode, int editX, int editY, + int editButtons, int x1, int y1, int x2, int y2, boolean rtl); + QtInputConnection.QtInputConnectionListener getInputConnectionListener(); +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtLayout.java b/src/android/jar/src/org/qtproject/qt/android/QtLayout.java index d7207dc2c5..aedc845014 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtLayout.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtLayout.java @@ -1,4 +1,4 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -6,48 +6,19 @@ package org.qtproject.qt.android; import android.app.Activity; import android.content.Context; -import android.graphics.Rect; import android.os.Build; -import android.util.Log; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Display; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.graphics.Insets; -import android.view.WindowMetrics; -import android.content.res.Configuration; -import android.content.res.Resources; -public class QtLayout extends ViewGroup -{ - private Runnable m_startApplicationRunnable; +class QtLayout extends ViewGroup { - private int m_activityDisplayRotation = -1; - private int m_ownDisplayRotation = -1; - private int m_nativeOrientation = -1; - - public void setActivityDisplayRotation(int rotation) - { - m_activityDisplayRotation = rotation; - } - - public void setNativeOrientation(int orientation) - { - m_nativeOrientation = orientation; - } - - public int displayRotation() - { - return m_ownDisplayRotation; - } - - public QtLayout(Context context, Runnable startRunnable) + public QtLayout(Context context) { super(context); - m_startApplicationRunnable = startRunnable; } public QtLayout(Context context, AttributeSet attrs) @@ -61,74 +32,6 @@ public class QtLayout extends ViewGroup } @Override - protected void onSizeChanged (int w, int h, int oldw, int oldh) - { - Activity activity = (Activity)getContext(); - if (activity == null) - return; - - final WindowManager windowManager = activity.getWindowManager(); - Display display; - - final WindowInsets rootInsets = getRootWindowInsets(); - - int insetLeft = 0; - int insetTop = 0; - - int maxWidth = 0; - int maxHeight = 0; - - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - display = windowManager.getDefaultDisplay(); - - final DisplayMetrics maxMetrics = new DisplayMetrics(); - display.getRealMetrics(maxMetrics); - maxWidth = maxMetrics.widthPixels; - maxHeight = maxMetrics.heightPixels; - - insetLeft = rootInsets.getStableInsetLeft(); - insetTop = rootInsets.getStableInsetTop(); - } else { - display = activity.getDisplay(); - - final WindowMetrics maxMetrics = windowManager.getMaximumWindowMetrics(); - maxWidth = maxMetrics.getBounds().width(); - maxHeight = maxMetrics.getBounds().height(); - - insetLeft = rootInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()).left; - insetTop = rootInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()).top; - } - - final DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics(); - double xdpi = displayMetrics.xdpi; - double ydpi = displayMetrics.ydpi; - double density = displayMetrics.density; - double scaledDensity = displayMetrics.scaledDensity; - float refreshRate = display.getRefreshRate(); - - QtNative.setApplicationDisplayMetrics(maxWidth, maxHeight, insetLeft, - insetTop, w, h, - xdpi,ydpi,scaledDensity, density, - refreshRate); - - int newRotation = display.getRotation(); - if (m_ownDisplayRotation != m_activityDisplayRotation - && newRotation == m_activityDisplayRotation) { - // If the saved rotation value does not match the one from the - // activity, it means that we got orientation change before size - // change, and the value was cached. So we need to notify about - // orientation change now. - QtNative.handleOrientationChanged(newRotation, m_nativeOrientation); - } - m_ownDisplayRotation = newRotation; - - if (m_startApplicationRunnable != null) { - m_startApplicationRunnable.run(); - m_startApplicationRunnable = null; - } - } - - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); @@ -146,11 +49,15 @@ public class QtLayout extends ViewGroup int childRight; int childBottom; - QtLayout.LayoutParams lp - = (QtLayout.LayoutParams) child.getLayoutParams(); - - childRight = lp.x + child.getMeasuredWidth(); - childBottom = lp.y + child.getMeasuredHeight(); + if (child.getLayoutParams() instanceof QtLayout.LayoutParams) { + QtLayout.LayoutParams lp + = (QtLayout.LayoutParams) child.getLayoutParams(); + childRight = lp.x + child.getMeasuredWidth(); + childBottom = lp.y + child.getMeasuredHeight(); + } else { + childRight = child.getMeasuredWidth(); + childBottom = child.getMeasuredHeight(); + } maxWidth = Math.max(maxWidth, childRight); maxHeight = Math.max(maxHeight, childBottom); @@ -184,7 +91,6 @@ public class QtLayout extends ViewGroup protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); - for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { @@ -193,10 +99,11 @@ public class QtLayout extends ViewGroup int childLeft = lp.x; int childTop = lp.y; - child.layout(childLeft, childTop, - childLeft + child.getMeasuredWidth(), - childTop + child.getMeasuredHeight()); - + int childRight = (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) ? + r - l : childLeft + child.getMeasuredWidth(); + int childBottom = (lp.height == ViewGroup.LayoutParams.MATCH_PARENT) ? + b - t : childTop + child.getMeasuredHeight(); + child.layout(childLeft, childTop, childRight, childBottom); } } } @@ -216,8 +123,7 @@ public class QtLayout extends ViewGroup /** * Per-child layout information associated with AbsoluteLayout. - * See - * {@link android.R.styleable#AbsoluteLayout_Layout Absolute Layout Attributes} + * See {android.R.styleable#AbsoluteLayout_Layout Absolute Layout Attributes} * for a list of all child view attributes that this class supports. */ public static class LayoutParams extends ViewGroup.LayoutParams @@ -249,6 +155,11 @@ public class QtLayout extends ViewGroup this.y = y; } + public LayoutParams(int width, int height) + { + super(width, height); + } + /** * {@inheritDoc} */ @@ -274,7 +185,7 @@ public class QtLayout extends ViewGroup /** * set the layout params on a child view. - * + * <p> * Note: This function adds the child view if it's not in the * layout already. */ diff --git a/src/android/jar/src/org/qtproject/qt/android/QtLayoutInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtLayoutInterface.java new file mode 100644 index 0000000000..8444266893 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtLayoutInterface.java @@ -0,0 +1,8 @@ +// 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; + +@UsedFromNativeCode +interface QtLayoutInterface { + QtLayout getQtLayout(); +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java new file mode 100644 index 0000000000..a00c4795f7 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java @@ -0,0 +1,558 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (c) 2019, BogDan Vatra <bogdan@kde.org> +// 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.annotation.SuppressLint; +import android.app.Activity; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ComponentInfo; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Objects; + +import dalvik.system.DexClassLoader; + +abstract class QtLoader { + + protected static final String QtTAG = "QtLoader"; + + private final Resources m_resources; + private final String m_packageName; + private String m_preferredAbi = null; + private String m_nativeLibrariesDir = null; + private ClassLoader m_classLoader; + + protected final ContextWrapper m_context; + protected ComponentInfo m_contextInfo; + + protected String m_mainLibPath; + protected String m_mainLibName; + protected String m_applicationParameters = ""; + protected HashMap<String, String> m_environmentVariables = new HashMap<>(); + + protected int m_debuggerSleepMs = 0; + + /** + * Sets and initialize the basic pieces. + * Initializes the class loader since it doesn't rely on anything + * other than the context. + * Also, we can already initialize the static classes contexts here. + **/ + public QtLoader(ContextWrapper context) { + m_context = context; + m_resources = m_context.getResources(); + m_packageName = m_context.getPackageName(); + + initClassLoader(); + initStaticClasses(); + initContextInfo(); + } + + /** + * Implements the logic for finish the extended context, mostly called + * in error cases. + **/ + abstract protected void finish(); + + /** + * Initializes the context info instance which is used to retrieve + * the app metadata from the AndroidManifest.xml or other xml resources. + * Some values are dependent on the context being an Activity or Service. + **/ + protected void initContextInfo() { + try { + Context context = m_context.getBaseContext(); + if (context instanceof Activity) { + m_contextInfo = context.getPackageManager().getActivityInfo( + ((Activity)context).getComponentName(), PackageManager.GET_META_DATA); + } else if (context instanceof Service) { + m_contextInfo = context.getPackageManager().getServiceInfo( + new ComponentName(context, context.getClass()), + PackageManager.GET_META_DATA); + } else { + Log.w(QtTAG, "Context is not an instance of Activity or Service, could not get " + + "context info for it"); + } + } catch (Exception e) { + e.printStackTrace(); + finish(); + } + } + + /** + * Extract the common metadata in the base implementation. And the extended methods + * call context specific metadata extraction. This also sets the various environment + * variables and application parameters. + **/ + protected void extractContextMetaData() { + setEnvironmentVariable("QT_ANDROID_FONTS", "Roboto;Droid Sans;Droid Sans Fallback"); + String monospaceFonts = "Droid Sans Mono;Droid Sans;Droid Sans Fallback"; + setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", monospaceFonts); + setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif"); + setEnvironmentVariable("HOME", m_context.getFilesDir().getAbsolutePath()); + setEnvironmentVariable("TMPDIR", m_context.getCacheDir().getAbsolutePath()); + String backgroundRunning = getMetaData("android.app.background_running"); + setEnvironmentVariable("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED", backgroundRunning); + setEnvironmentVariable("QTRACE_LOCATION", getMetaData("android.app.trace_location")); + appendApplicationParameters(getMetaData("android.app.arguments")); + } + + private ArrayList<String> preferredAbiLibs(String[] libs) { + HashMap<String, ArrayList<String>> abiLibs = new HashMap<>(); + for (String lib : libs) { + String[] archLib = lib.split(";", 2); + if (m_preferredAbi != null && !archLib[0].equals(m_preferredAbi)) + continue; + if (!abiLibs.containsKey(archLib[0])) + abiLibs.put(archLib[0], new ArrayList<>()); + Objects.requireNonNull(abiLibs.get(archLib[0])).add(archLib[1]); + } + + if (m_preferredAbi != null) { + if (abiLibs.containsKey(m_preferredAbi)) { + return abiLibs.get(m_preferredAbi); + } + return new ArrayList<>(); + } + + for (String abi : Build.SUPPORTED_ABIS) { + if (abiLibs.containsKey(abi)) { + m_preferredAbi = abi; + return abiLibs.get(abi); + } + } + return new ArrayList<>(); + } + + private void initStaticClasses() { + Context context = m_context.getBaseContext(); + boolean isActivity = context instanceof Activity; + for (String className : getStaticInitClasses()) { + try { + Class<?> initClass = m_classLoader.loadClass(className); + Object staticInitDataObject = initClass.newInstance(); // create an instance + + if (isActivity) { + try { + Method m = initClass.getMethod("setActivity", Activity.class, Object.class); + m.invoke(staticInitDataObject, (Activity) context, this); + } catch (InvocationTargetException | NoSuchMethodException e) { + Log.d(QtTAG, "Class " + initClass.getName() + " does not implement " + + "setActivity method"); + } + } else { + try { + Method m = initClass.getMethod("setService", Service.class, Object.class); + m.invoke(staticInitDataObject, (Service) context, this); + } catch (InvocationTargetException | NoSuchMethodException e) { + Log.d(QtTAG, "Class " + initClass.getName() + " does not implement " + + "setService method"); + } + } + + try { + // For modules that don't need/have setActivity/setService + Method m = initClass.getMethod("setContext", Context.class); + m.invoke(staticInitDataObject, context); + } catch (InvocationTargetException | NoSuchMethodException e) { + Log.d(QtTAG, "Class " + initClass.getName() + " does not implement " + + "setContext method"); + } + } catch (IllegalAccessException | ClassNotFoundException | InstantiationException e) { + Log.d(QtTAG, "Could not instantiate class " + className + ", " + e); + } + } + } + + /** + * Initialize the class loader instance and sets it via QtNative. + * This would also be used by QJniObject API. + **/ + private void initClassLoader() + { + // directory where optimized DEX files should be written. + String outDexPath = m_context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath(); + String sourceDir = m_context.getApplicationInfo().sourceDir; + m_classLoader = new DexClassLoader(sourceDir, outDexPath, null, m_context.getClassLoader()); + QtNative.setClassLoader(m_classLoader); + } + + /** + * Returns the context's main library absolute path, + * or null if the library hasn't been loaded yet. + **/ + public String getMainLibraryPath() { + return m_mainLibPath; + } + + /** + * Set the name of the main app library to libName, which is the name of the library, + * not including the path, target architecture or .so suffix. This matches the target name + * of the app target in CMakeLists.txt. + * This method can be used when the name is not provided by androiddeployqt, for example when + * embedding QML views to a native Android app. + **/ + public void setMainLibraryName(String libName) { + m_mainLibName = libName; + } + + /** + * Returns the context's parameters that are used when calling + * the main library's main() function. This is assembled from + * a combination of static values and also metadata dependent values. + **/ + public String getApplicationParameters() { + return m_applicationParameters; + } + + /** + * Adds a list of parameters to the internal array list of parameters. + * Either a whitespace or a tab is accepted as a separator between parameters. + **/ + public void appendApplicationParameters(String params) + { + if (params == null || params.isEmpty()) + return; + + if (!m_applicationParameters.isEmpty()) + m_applicationParameters += " "; + m_applicationParameters += params; + } + + /** + * Sets a single key/value environment variable pair. + **/ + public void setEnvironmentVariable(String key, String value) + { + try { + android.system.Os.setenv(key, value, true); + m_environmentVariables.put(key, value); + } catch (Exception e) { + Log.e(QtTAG, "Could not set environment variable:" + key + "=" + value); + e.printStackTrace(); + } + } + + /** + * Sets a list of keys/values string to as environment variables. + * This expects the key/value to be separated by '=', and parameters + * to be separated by tabs or space. + **/ + public void setEnvironmentVariables(String environmentVariables) + { + if (environmentVariables == null || environmentVariables.isEmpty()) + return; + + environmentVariables = environmentVariables.replaceAll("\t", " "); + + for (String variable : environmentVariables.split(" ")) { + String[] keyValue = variable.split("=", 2); + if (keyValue.length < 2 || keyValue[0].isEmpty()) + continue; + + setEnvironmentVariable(keyValue[0], keyValue[1]); + } + } + + /** + * Parses the native libraries dir. If the libraries are part of the APK, + * the path is set to the APK extracted libs path. + * Otherwise, it looks for the system level dir, that's either set in the Manifest, + * the deployment libs.xml. + * If none of the above are valid, it falls back to predefined system path. + **/ + private void parseNativeLibrariesDir() { + if (isBundleQtLibs()) { + String nativeLibraryPrefix = m_context.getApplicationInfo().nativeLibraryDir + "/"; + File nativeLibraryDir = new File(nativeLibraryPrefix); + if (nativeLibraryDir.exists()) { + String[] list = nativeLibraryDir.list(); + if (nativeLibraryDir.isDirectory() && list != null && list.length > 0) { + m_nativeLibrariesDir = nativeLibraryPrefix; + } + } + } else { + // First check if user has provided system libs prefix in AndroidManifest + String systemLibsPrefix = getApplicationMetaData("android.app.system_libs_prefix"); + + // If not, check if it's provided by androiddeployqt in libs.xml + if (systemLibsPrefix.isEmpty()) + systemLibsPrefix = getSystemLibsPrefix(); + + if (systemLibsPrefix.isEmpty()) { + final String SYSTEM_LIB_PATH = "/system/lib/"; + systemLibsPrefix = SYSTEM_LIB_PATH; + Log.e(QtTAG, "Using " + SYSTEM_LIB_PATH + " as default libraries path. " + + "It looks like the app is deployed using Unbundled " + + "deployment. It may be necessary to specify the path to " + + "the directory where Qt libraries are installed using either " + + "android.app.system_libs_prefix metadata variable in your " + + "AndroidManifest.xml or QT_ANDROID_SYSTEM_LIBS_PATH in your " + + "CMakeLists.txt"); + } + + File systemLibraryDir = new File(systemLibsPrefix); + String[] list = systemLibraryDir.list(); + if (systemLibraryDir.exists()) { + if (systemLibraryDir.isDirectory() && list != null && list.length > 0) + m_nativeLibrariesDir = systemLibsPrefix; + else + Log.e(QtTAG, "System library directory " + systemLibsPrefix + " is empty."); + } else { + Log.e(QtTAG, "System library directory " + systemLibsPrefix + " does not exist."); + } + } + + if (m_nativeLibrariesDir != null && !m_nativeLibrariesDir.endsWith("/")) + m_nativeLibrariesDir += "/"; + } + + /** + * Returns the application level metadata. + * + * @noinspection SameParameterValue*/ + private String getApplicationMetaData(String key) { + if (m_contextInfo == null) + return ""; + + ApplicationInfo applicationInfo = m_contextInfo.applicationInfo; + if (applicationInfo == null) + return ""; + + Bundle metadata = applicationInfo.metaData; + if (metadata == null || !metadata.containsKey(key)) + return ""; + + return metadata.getString(key); + } + + /** + * Returns the context level metadata. + **/ + protected String getMetaData(String key) { + if (m_contextInfo == null) + return ""; + + Bundle metadata = m_contextInfo.metaData; + if (metadata == null || !metadata.containsKey(key)) + return ""; + + return String.valueOf(metadata.get(key)); + } + + @SuppressLint("DiscouragedApi") + private ArrayList<String> getQtLibrariesList() { + int id = m_resources.getIdentifier("qt_libs", "array", m_packageName); + return preferredAbiLibs(m_resources.getStringArray(id)); + } + + @SuppressLint("DiscouragedApi") + private boolean useLocalQtLibs() { + int id = m_resources.getIdentifier("use_local_qt_libs", "string", m_packageName); + return Integer.parseInt(m_resources.getString(id)) == 1; + } + + @SuppressLint("DiscouragedApi") + private boolean isBundleQtLibs() { + int id = m_resources.getIdentifier("bundle_local_qt_libs", "string", m_packageName); + return Integer.parseInt(m_resources.getString(id)) == 1; + } + + @SuppressLint("DiscouragedApi") + private String getSystemLibsPrefix() { + int id = m_resources.getIdentifier("system_libs_prefix", "string", m_packageName); + return m_resources.getString(id); + } + + @SuppressLint("DiscouragedApi") + private ArrayList<String> getLocalLibrariesList() { + int id = m_resources.getIdentifier("load_local_libs", "array", m_packageName); + ArrayList<String> localLibs = new ArrayList<>(); + for (String arrayItem : preferredAbiLibs(m_resources.getStringArray(id))) { + Collections.addAll(localLibs, arrayItem.split(":")); + } + return localLibs; + } + + @SuppressLint("DiscouragedApi") + private ArrayList<String> getStaticInitClasses() { + int id = m_resources.getIdentifier("static_init_classes", "string", m_packageName); + String[] classes = m_resources.getString(id).split(":"); + ArrayList<String> finalClasses = new ArrayList<>(); + for (String element : classes) { + if (!element.isEmpty()) { + finalClasses.add(element); + } + } + return finalClasses; + } + + @SuppressLint("DiscouragedApi") + private String[] getBundledLibs() { + int id = m_resources.getIdentifier("bundled_libs", "array", m_packageName); + return m_resources.getStringArray(id); + } + + /** + * Loads all Qt native bundled libraries and main library. + **/ + public void loadQtLibraries() { + if (!useLocalQtLibs()) { + Log.w(QtTAG, "Use local Qt libs is false"); + finish(); + return; + } + + if (m_nativeLibrariesDir == null) + parseNativeLibrariesDir(); + + if (m_nativeLibrariesDir == null || m_nativeLibrariesDir.isEmpty()) { + Log.e(QtTAG, "The native libraries directory is null or empty"); + finish(); + return; + } + + setEnvironmentVariable("QT_PLUGIN_PATH", m_nativeLibrariesDir); + setEnvironmentVariable("QML_PLUGIN_PATH", m_nativeLibrariesDir); + + // Load native Qt APK libraries + ArrayList<String> nativeLibraries = getQtLibrariesList(); + nativeLibraries.addAll(getLocalLibrariesList()); + + if (m_debuggerSleepMs > 0) { + Log.i(QtTAG, "Sleeping for " + m_debuggerSleepMs + + "ms, helping the native debugger to settle. " + + "Use the env QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS variable to change this value."); + QtNative.getQtThread().sleep(m_debuggerSleepMs); + } + + if (!loadLibraries(nativeLibraries)) { + Log.e(QtTAG, "Loading Qt native libraries failed"); + finish(); + return; + } + + // add all bundled Qt libs to loader params + ArrayList<String> bundledLibraries = new ArrayList<>(preferredAbiLibs(getBundledLibs())); + if (!loadLibraries(bundledLibraries)) { + Log.e(QtTAG, "Loading Qt bundled libraries failed"); + finish(); + return; + } + + if (m_mainLibName == null) + m_mainLibName = getMetaData("android.app.lib_name"); + // Load main lib + if (!loadMainLibrary(m_mainLibName + "_" + m_preferredAbi)) { + Log.e(QtTAG, "Loading main library failed"); + finish(); + } + } + + // Loading libraries using System.load() uses full lib paths + @SuppressLint("UnsafeDynamicallyLoadedCode") + private String loadLibraryHelper(String library) + { + String loadedLib = null; + try { + File libFile = new File(library); + if (libFile.exists()) { + System.load(library); + loadedLib = library; + } else { + Log.e(QtTAG, "Can't find '" + library + "'"); + } + } catch (Exception e) { + Log.e(QtTAG, "Can't load '" + library + "'", e); + } + + return loadedLib; + } + + /** + * Returns an array with absolute library paths from a list of file names only. + **/ + private ArrayList<String> getLibrariesFullPaths(final ArrayList<String> libraries) + { + if (libraries == null) + return null; + + ArrayList<String> absolutePathLibraries = new ArrayList<>(); + for (String libName : libraries) { + // Add lib and .so to the lib name only if it doesn't already end with .so, + // this means some names don't necessarily need to have the lib prefix + if (!libName.endsWith(".so")) { + libName = libName + ".so"; + libName = "lib" + libName; + } + + File file = new File(m_nativeLibrariesDir + libName); + absolutePathLibraries.add(file.getAbsolutePath()); + } + + return absolutePathLibraries; + } + + /** + * Loads the main library. + * Returns true if loading was successful, and sets the absolute + * path to the main library. Otherwise, returns false and the path + * to the main library is null. + **/ + private boolean loadMainLibrary(String mainLibName) + { + ArrayList<String> oneEntryArray = new ArrayList<>(Collections.singletonList(mainLibName)); + String mainLibPath = getLibrariesFullPaths(oneEntryArray).get(0); + final boolean[] success = {true}; + QtNative.getQtThread().run(() -> { + m_mainLibPath = loadLibraryHelper(mainLibPath); + if (m_mainLibPath == null) + success[0] = false; + }); + + return success[0]; + } + + /** + * Loads a list of libraries. + * Returns true if all libraries were loaded successfully, + * and false if any library failed. Stops loading at the first failure. + **/ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean loadLibraries(final ArrayList<String> libraries) + { + if (libraries == null) + return false; + + ArrayList<String> fullPathLibs = getLibrariesFullPaths(libraries); + + final boolean[] success = {true}; + QtNative.getQtThread().run(() -> { + for (int i = 0; i < fullPathLibs.size(); ++i) { + String libName = fullPathLibs.get(i); + if (loadLibraryHelper(libName) == null) { + success[0] = false; + break; + } + } + }); + + return success[0]; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtMenuInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtMenuInterface.java new file mode 100644 index 0000000000..556b9a57b9 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtMenuInterface.java @@ -0,0 +1,11 @@ +// 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; + +@UsedFromNativeCode +interface QtMenuInterface { + void resetOptionsMenu(); + void openOptionsMenu(); + void closeContextMenu(); + void openContextMenu(final int x, final int y, final int w, final int h); +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java b/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java index 26696577b1..e13abbbadd 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java @@ -6,24 +6,23 @@ package org.qtproject.qt.android; import android.app.Activity; import android.app.AlertDialog; +import android.content.ClipData; import android.content.Context; -import android.content.DialogInterface; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; -import android.text.ClipboardManager; +import android.content.ClipboardManager; import android.text.Html; import android.text.Spanned; +import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.Window; import android.widget.Button; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; -import android.widget.Toast; import java.util.ArrayList; @@ -41,7 +40,7 @@ class ButtonStruct implements View.OnClickListener m_text = Html.fromHtml(text); } QtMessageDialogHelper m_dialog; - private int m_id; + private final int m_id; Spanned m_text; @Override @@ -50,7 +49,7 @@ class ButtonStruct implements View.OnClickListener } } -public class QtMessageDialogHelper +class QtMessageDialogHelper { public QtMessageDialogHelper(Activity activity) @@ -58,7 +57,7 @@ public class QtMessageDialogHelper m_activity = activity; } - + @UsedFromNativeCode public void setStandardIcon(int icon) { m_standardIcon = icon; @@ -70,262 +69,242 @@ public class QtMessageDialogHelper if (m_standardIcon == 0) return null; - try { - TypedValue typedValue = new TypedValue(); - m_theme.resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true); - return m_activity.getResources().getDrawable(typedValue.resourceId, - m_activity.getTheme()); - } catch (Exception e) { - e.printStackTrace(); - } - // Information, Warning, Critical, Question switch (m_standardIcon) { case 1: // Information - try { - return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_info, - m_activity.getTheme()); - } catch (Exception e) { - e.printStackTrace(); - } - break; + return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_info, + m_activity.getTheme()); case 2: // Warning -// try { -// return Class.forName("android.R$drawable").getDeclaredField("stat_sys_warning").getInt(null); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// break; + return m_activity.getResources().getDrawable(android.R.drawable.stat_sys_warning, + m_activity.getTheme()); case 3: // Critical - try { - return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_alert, - m_activity.getTheme()); - } catch (Exception e) { - e.printStackTrace(); - } - break; + return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_alert, + m_activity.getTheme()); case 4: // Question - try { - return m_activity.getResources().getDrawable(android.R.drawable.ic_menu_help, - m_activity.getTheme()); - } catch (Exception e) { - e.printStackTrace(); - } - break; + return m_activity.getResources().getDrawable(android.R.drawable.ic_menu_help, + m_activity.getTheme()); } return null; } + @UsedFromNativeCode public void setTile(String title) { m_title = Html.fromHtml(title); } + @UsedFromNativeCode public void setText(String text) { m_text = Html.fromHtml(text); } + @UsedFromNativeCode public void setInformativeText(String informativeText) { m_informativeText = Html.fromHtml(informativeText); } + @UsedFromNativeCode public void setDetailedText(String text) { m_detailedText = Html.fromHtml(text); } + @UsedFromNativeCode public void addButton(int id, String text) { if (m_buttonsList == null) - m_buttonsList = new ArrayList<ButtonStruct>(); + m_buttonsList = new ArrayList<>(); m_buttonsList.add(new ButtonStruct(this, id, text)); } - private Drawable getStyledDrawable(String drawable) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException + private Drawable getStyledDrawable(int id) { - int[] attrs = {Class.forName("android.R$attr").getDeclaredField(drawable).getInt(null)}; + int[] attrs = { id }; final TypedArray a = m_theme.obtainStyledAttributes(attrs); Drawable d = a.getDrawable(0); a.recycle(); return d; } - + @UsedFromNativeCode public void show(long handler) { m_handler = handler; - m_activity.runOnUiThread( new Runnable() { - @Override - public void run() { - if (m_dialog != null && m_dialog.isShowing()) - m_dialog.dismiss(); - - m_dialog = new AlertDialog.Builder(m_activity).create(); - m_theme = m_dialog.getWindow().getContext().getTheme(); - - if (m_title != null) - m_dialog.setTitle(m_title); - m_dialog.setOnCancelListener( new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - QtNativeDialogHelper.dialogResult(handler(), -1); - } - }); - m_dialog.setCancelable(m_buttonsList == null); - m_dialog.setCanceledOnTouchOutside(m_buttonsList == null); - m_dialog.setIcon(getIconDrawable()); - ScrollView scrollView = new ScrollView(m_activity); - RelativeLayout dialogLayout = new RelativeLayout(m_activity); - int id = 1; - View lastView = null; - View.OnLongClickListener copyText = new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - TextView tv = (TextView)view; - if (tv != null) { - ClipboardManager cm = (android.text.ClipboardManager) m_activity.getSystemService(Context.CLIPBOARD_SERVICE); - cm.setText(tv.getText()); - } - return true; - } - }; - if (m_text != null) - { - TextView view = new TextView(m_activity); - view.setId(id++); - view.setOnLongClickListener(copyText); - view.setLongClickable(true); - - view.setText(m_text); - view.setTextAppearance(m_activity, android.R.style.TextAppearance_Medium); + m_activity.runOnUiThread(() -> { + if (m_dialog != null && m_dialog.isShowing()) + m_dialog.dismiss(); + + m_dialog = new AlertDialog.Builder(m_activity).create(); + Window window = m_dialog.getWindow(); + if (window != null) + m_theme = window.getContext().getTheme(); + else + Log.w(QtTAG, "show(): cannot set theme from null window!"); + + if (m_title != null) + m_dialog.setTitle(m_title); + m_dialog.setOnCancelListener(dialogInterface -> QtNativeDialogHelper.dialogResult(handler(), -1)); + m_dialog.setCancelable(m_buttonsList == null); + m_dialog.setCanceledOnTouchOutside(m_buttonsList == null); + m_dialog.setIcon(getIconDrawable()); + ScrollView scrollView = new ScrollView(m_activity); + RelativeLayout dialogLayout = new RelativeLayout(m_activity); + int id = 1; + View lastView = null; + View.OnLongClickListener copyText = view -> { + TextView tv = (TextView)view; + if (tv != null) { + ClipboardManager cm = (ClipboardManager) m_activity.getSystemService( + Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newPlainText(tv.getText(), tv.getText())); + } + return true; + }; + if (m_text != null) + { + TextView view = new TextView(m_activity); + view.setId(id++); + view.setOnLongClickListener(copyText); + view.setLongClickable(true); + + view.setText(m_text); + view.setTextAppearance(android.R.style.TextAppearance_Medium); + + RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + layout.setMargins(16, 8, 16, 8); + layout.addRule(RelativeLayout.ALIGN_PARENT_TOP); + dialogLayout.addView(view, layout); + lastView = view; + } - RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); - layout.setMargins(16, 8, 16, 8); + if (m_informativeText != null) + { + TextView view= new TextView(m_activity); + view.setId(id++); + view.setOnLongClickListener(copyText); + view.setLongClickable(true); + + view.setText(m_informativeText); + view.setTextAppearance(android.R.style.TextAppearance_Medium); + + RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + layout.setMargins(16, 8, 16, 8); + if (lastView != null) + layout.addRule(RelativeLayout.BELOW, lastView.getId()); + else layout.addRule(RelativeLayout.ALIGN_PARENT_TOP); - dialogLayout.addView(view, layout); - lastView = view; - } + dialogLayout.addView(view, layout); + lastView = view; + } - if (m_informativeText != null) - { - TextView view= new TextView(m_activity); - view.setId(id++); - view.setOnLongClickListener(copyText); - view.setLongClickable(true); - - view.setText(m_informativeText); - view.setTextAppearance(m_activity, android.R.style.TextAppearance_Medium); - - RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); - layout.setMargins(16, 8, 16, 8); - if (lastView != null) - layout.addRule(RelativeLayout.BELOW, lastView.getId()); - else - layout.addRule(RelativeLayout.ALIGN_PARENT_TOP); - dialogLayout.addView(view, layout); - lastView = view; - } + if (m_detailedText != null) + { + TextView view= new TextView(m_activity); + view.setId(id++); + view.setOnLongClickListener(copyText); + view.setLongClickable(true); + + view.setText(m_detailedText); + view.setTextAppearance(android.R.style.TextAppearance_Small); + + RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + layout.setMargins(16, 8, 16, 8); + if (lastView != null) + layout.addRule(RelativeLayout.BELOW, lastView.getId()); + else + layout.addRule(RelativeLayout.ALIGN_PARENT_TOP); + dialogLayout.addView(view, layout); + lastView = view; + } - if (m_detailedText != null) + if (m_buttonsList != null) + { + LinearLayout buttonsLayout = new LinearLayout(m_activity); + buttonsLayout.setOrientation(LinearLayout.HORIZONTAL); + buttonsLayout.setId(id++); + boolean firstButton = true; + for (ButtonStruct button: m_buttonsList) { - TextView view= new TextView(m_activity); - view.setId(id++); - view.setOnLongClickListener(copyText); - view.setLongClickable(true); - - view.setText(m_detailedText); - view.setTextAppearance(m_activity, android.R.style.TextAppearance_Small); - - RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); - layout.setMargins(16, 8, 16, 8); - if (lastView != null) - layout.addRule(RelativeLayout.BELOW, lastView.getId()); - else - layout.addRule(RelativeLayout.ALIGN_PARENT_TOP); - dialogLayout.addView(view, layout); - lastView = view; - } + Button bv; + try { + bv = new Button(m_activity, null, android.R.attr.borderlessButtonStyle); + } catch (Exception e) { + bv = new Button(m_activity); + e.printStackTrace(); + } - if (m_buttonsList != null) - { - LinearLayout buttonsLayout = new LinearLayout(m_activity); - buttonsLayout.setOrientation(LinearLayout.HORIZONTAL); - buttonsLayout.setId(id++); - boolean firstButton = true; - for (ButtonStruct button: m_buttonsList) + bv.setText(button.m_text); + bv.setOnClickListener(button); + if (!firstButton) // first button { - Button bv; + View spacer = new View(m_activity); try { - bv = new Button(m_activity, null, Class.forName("android.R$attr").getDeclaredField("borderlessButtonStyle").getInt(null)); + LinearLayout.LayoutParams layout = new LinearLayout.LayoutParams(1, + RelativeLayout.LayoutParams.MATCH_PARENT); + spacer.setBackground(getStyledDrawable(android.R.attr.dividerVertical)); + buttonsLayout.addView(spacer, layout); } catch (Exception e) { - bv = new Button(m_activity); e.printStackTrace(); } - - bv.setText(button.m_text); - bv.setOnClickListener(button); - if (!firstButton) // first button - { - LinearLayout.LayoutParams layout = null; - View spacer = new View(m_activity); - try { - layout = new LinearLayout.LayoutParams(1, RelativeLayout.LayoutParams.MATCH_PARENT); - spacer.setBackgroundDrawable(getStyledDrawable("dividerVertical")); - buttonsLayout.addView(spacer, layout); - } catch (Exception e) { - e.printStackTrace(); - } - } - LinearLayout.LayoutParams layout = null; - layout = new LinearLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT, 1.0f); - buttonsLayout.addView(bv, layout); - firstButton = false; } + LinearLayout.LayoutParams layout = new LinearLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, 1.0f); + buttonsLayout.addView(bv, layout); + firstButton = false; + } - try { - View horizontalDevider = new View(m_activity); - horizontalDevider.setId(id++); - horizontalDevider.setBackgroundDrawable(getStyledDrawable("dividerHorizontal")); - RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, 1); - relativeParams.setMargins(0, 10, 0, 0); - if (lastView != null) { - relativeParams.addRule(RelativeLayout.BELOW, lastView.getId()); - } - else - relativeParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); - dialogLayout.addView(horizontalDevider, relativeParams); - lastView = horizontalDevider; - } catch (Exception e) { - e.printStackTrace(); - } - RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + try { + View horizontalDivider = new View(m_activity); + horizontalDivider.setId(id); + horizontalDivider.setBackground(getStyledDrawable( + android.R.attr.dividerHorizontal)); + RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, 1); + relativeParams.setMargins(0, 10, 0, 0); if (lastView != null) { relativeParams.addRule(RelativeLayout.BELOW, lastView.getId()); } else relativeParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); - relativeParams.setMargins(2, 0, 2, 0); - dialogLayout.addView(buttonsLayout, relativeParams); + dialogLayout.addView(horizontalDivider, relativeParams); + lastView = horizontalDivider; + } catch (Exception e) { + e.printStackTrace(); + } + RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + if (lastView != null) { + relativeParams.addRule(RelativeLayout.BELOW, lastView.getId()); } - scrollView.addView(dialogLayout); - m_dialog.setView(scrollView); - m_dialog.show(); + else + relativeParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + relativeParams.setMargins(2, 0, 2, 0); + dialogLayout.addView(buttonsLayout, relativeParams); } + scrollView.addView(dialogLayout); + m_dialog.setView(scrollView); + m_dialog.show(); }); } + @UsedFromNativeCode public void hide() { - m_activity.runOnUiThread( new Runnable() { - @Override - public void run() { - if (m_dialog != null && m_dialog.isShowing()) - m_dialog.dismiss(); - reset(); - } + m_activity.runOnUiThread(() -> { + if (m_dialog != null && m_dialog.isShowing()) + m_dialog.dismiss(); + reset(); }); } @@ -346,7 +325,8 @@ public class QtMessageDialogHelper m_handler = 0; } - private Activity m_activity; + private static final String QtTAG = "QtMessageDialogHelper"; + private final Activity m_activity; private int m_standardIcon = 0; private Spanned m_title, m_text, m_informativeText, m_detailedText; private ArrayList<ButtonStruct> m_buttonsList; diff --git a/src/android/jar/src/org/qtproject/qt/android/QtNative.java b/src/android/jar/src/org/qtproject/qt/android/QtNative.java index 93bc6d8043..17e1386efb 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtNative.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtNative.java @@ -1,146 +1,116 @@ // Copyright (C) 2016 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 The Qt Company Ltd. +// 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 package org.qtproject.qt.android; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.Semaphore; - import android.app.Activity; import android.app.Service; import android.content.Context; -import android.content.ContentResolver; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ApplicationInfo; import android.content.UriPermission; +import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.system.Os; -import android.content.ClipboardManager; -import android.content.ClipData; -import android.content.ClipDescription; -import android.os.ParcelFileDescriptor; import android.util.Log; import android.view.ContextMenu; -import android.view.KeyEvent; import android.view.Menu; -import android.view.MotionEvent; import android.view.View; -import android.view.InputDevice; -import android.view.Display; -import android.hardware.display.DisplayManager; -import android.database.Cursor; -import android.provider.DocumentsContract; -import java.lang.reflect.Method; +import java.lang.ref.WeakReference; import java.security.KeyStore; import java.security.cert.X509Certificate; -import java.util.Iterator; +import java.util.ArrayList; import java.util.List; -import javax.net.ssl.TrustManagerFactory; +import java.util.Objects; + import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +// ### Qt7: make private and find new API for onNewIntent() public class QtNative { - private static Activity m_activity = null; - private static boolean m_activityPaused = false; - private static Service m_service = null; - private static QtActivityDelegate m_activityDelegate = null; - private static QtServiceDelegate m_serviceDelegate = null; - public static Object m_mainActivityMutex = new Object(); // mutex used to synchronize runnable operations - - public static final String QtTAG = "Qt JAVA"; // string used for Log.x - private static ArrayList<Runnable> m_lostActions = new ArrayList<Runnable>(); // a list containing all actions which could not be performed (e.g. the main activity is destroyed, etc.) - private static boolean m_started = false; - private static boolean m_isKeyboardHiding = false; - private static int m_displayMetricsScreenWidthPixels = 0; - private static int m_displayMetricsScreenHeightPixels = 0; - private static int m_displayMetricsAvailableLeftPixels = 0; - private static int m_displayMetricsAvailableTopPixels = 0; - private static int m_displayMetricsAvailableWidthPixels = 0; - private static int m_displayMetricsAvailableHeightPixels = 0; - private static float m_displayMetricsRefreshRate = 60; - private static double m_displayMetricsXDpi = .0; - private static double m_displayMetricsYDpi = .0; - private static double m_displayMetricsScaledDensity = 1.0; - private static double m_displayMetricsDensity = 1.0; - private static int m_oldx, m_oldy; - private static final int m_moveThreshold = 0; - private static ClipboardManager m_clipboardManager = null; - private static Method m_checkSelfPermissionMethod = null; - private static Boolean m_tabletEventSupported = null; - private static boolean m_usePrimaryClip = false; - public static QtThread m_qtThread = new QtThread(); - private static final int KEYBOARD_HEIGHT_THRESHOLD = 100; - - private static final String INVALID_OR_NULL_URI_ERROR_MESSAGE = "Received invalid/null Uri"; - - private static final Runnable runPendingCppRunnablesRunnable = new Runnable() { - @Override - public void run() { - runPendingCppRunnables(); - } - }; + private static WeakReference<Activity> m_activity = null; + private static WeakReference<Service> m_service = null; + private static final Object m_mainActivityMutex = new Object(); // mutex used to synchronize runnable operations - public static boolean isStarted() - { - boolean hasActivity = m_activity != null && m_activityDelegate != null; - boolean hasService = m_service != null && m_serviceDelegate != null; - return m_started && (hasActivity || hasService); - } + private static final ApplicationStateDetails m_stateDetails = new ApplicationStateDetails(); + + static final String QtTAG = "Qt JAVA"; + + // a list containing all actions which could not be performed (e.g. the main activity is destroyed, etc.) + private static final ArrayList<Runnable> m_lostActions = new ArrayList<>(); + private static final QtThread m_qtThread = new QtThread(); private static ClassLoader m_classLoader = null; - public static ClassLoader classLoader() + + private static final Runnable runPendingCppRunnablesRunnable = QtNative::runPendingCppRunnables; + private static final ArrayList<AppStateDetailsListener> m_appStateListeners = new ArrayList<>(); + private static final Object m_appStateListenersLock = new Object(); + + @UsedFromNativeCode + static ClassLoader classLoader() { return m_classLoader; } - public static void setClassLoader(ClassLoader classLoader) + static void setClassLoader(ClassLoader classLoader) { m_classLoader = classLoader; } - public static Activity activity() + static void setActivity(Activity qtMainActivity) { synchronized (m_mainActivityMutex) { - return m_activity; + m_activity = new WeakReference<>(qtMainActivity); } } - public static Service service() + static void setService(Service qtMainService) { synchronized (m_mainActivityMutex) { - return m_service; + m_service = new WeakReference<>(qtMainService); } } - - public static QtActivityDelegate activityDelegate() + @UsedFromNativeCode + static Activity activity() { synchronized (m_mainActivityMutex) { - return m_activityDelegate; + return m_activity != null ? m_activity.get() : null; } } - public static QtServiceDelegate serviceDelegate() + static boolean isActivityValid() + { + return m_activity != null && m_activity.get() != null; + } + + @UsedFromNativeCode + static Service service() { synchronized (m_mainActivityMutex) { - return m_serviceDelegate; + return m_service != null ? m_service.get() : null; } } - public static String[] getStringArray(String joinedString) + static boolean isServiceValid() + { + return m_service != null && m_service.get() != null; + } + + @UsedFromNativeCode + static Context getContext() { + if (isActivityValid()) + return m_activity.get(); + return service(); + } + + @UsedFromNativeCode + static String[] getStringArray(String joinedString) { return joinedString.split(","); } @@ -150,6 +120,7 @@ public class QtNative return new Exception().getStackTrace()[1].getMethodName() + ": "; } + /** @noinspection SameParameterValue*/ private static Uri getUriWithValidPermission(Context context, String uri, String openMode) { Uri parsedUri; @@ -164,7 +135,7 @@ public class QtNative String scheme = parsedUri.getScheme(); // We only want to check permissions for content Uris - if (scheme.compareTo("content") != 0) + if (scheme != null && scheme.compareTo("content") != 0) return parsedUri; List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions(); @@ -177,7 +148,7 @@ public class QtNative if (!openMode.equals("r")) isRequestPermission = permissions.get(i).isWritePermission(); - if (iterUri.getPath().equals(uriStr) && isRequestPermission) + if (Objects.equals(iterUri.getPath(), uriStr) && isRequestPermission) return iterUri; } @@ -186,16 +157,17 @@ public class QtNative // and check for SecurityExceptions later return parsedUri; } catch (SecurityException e) { - Log.e(QtTAG, getCurrentMethodNameLog() + e.toString()); + Log.e(QtTAG, getCurrentMethodNameLog() + e); return parsedUri; } } - public static boolean openURL(Context context, String url, String mime) + @UsedFromNativeCode + static boolean openURL(Context context, String url, String mime) { final Uri uri = getUriWithValidPermission(context, url, "r"); if (uri == null) { - Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE); + Log.e(QtTAG, getCurrentMethodNameLog() + "received invalid/null Uri"); return false; } @@ -205,165 +177,137 @@ public class QtNative if (!mime.isEmpty()) intent.setDataAndType(uri, mime); - activity().startActivity(intent); + Activity activity = activity(); + if (activity == null) { + Log.w(QtTAG, "openURL(): The activity reference is null"); + return false; + } + + activity.startActivity(intent); return true; } catch (Exception e) { - Log.e(QtTAG, getCurrentMethodNameLog() + e.toString()); + Log.e(QtTAG, getCurrentMethodNameLog() + e); return false; } } - // this method loads full path libs - public static void loadQtLibraries(final ArrayList<String> libraries) - { - m_qtThread.run(new Runnable() { - @Override - public void run() { - if (libraries == null) - return; - for (String libName : libraries) { - try { - File f = new File(libName); - if (f.exists()) - System.load(libName); - else - Log.i(QtTAG, "Can't find '" + libName + "'"); - } catch (SecurityException e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } catch (Exception e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } - } - } - }); + static QtThread getQtThread() { + return m_qtThread; } - // this method loads bundled libs by name. - public static void loadBundledLibraries(final ArrayList<String> libraries, final String nativeLibraryDir) - { - m_qtThread.run(new Runnable() { - @Override - public void run() { - if (libraries == null) - return; + interface AppStateDetailsListener { + default void onAppStateDetailsChanged(ApplicationStateDetails details) {} + default void onNativePluginIntegrationReadyChanged(boolean ready) {} + } - for (String libName : libraries) { - try { - String libNameTemplate = "lib" + libName + ".so"; - File f = new File(nativeLibraryDir + libNameTemplate); - if (!f.exists()) { - Log.i(QtTAG, "Can't find '" + f.getAbsolutePath()); - try { - ApplicationInfo info = getContext().getApplicationContext().getPackageManager() - .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir; - if (info.metaData.containsKey("android.app.system_libs_prefix")) - systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix"); - f = new File(systemLibraryDir + libNameTemplate); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (f.exists()) - System.load(f.getAbsolutePath()); - else - Log.i(QtTAG, "Can't find '" + f.getAbsolutePath()); - } catch (Exception e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } - } - } - }); + // Keep in sync with src/corelib/global/qnamespace.h + static class ApplicationState { + static final int ApplicationSuspended = 0x0; + static final int ApplicationHidden = 0x1; + static final int ApplicationInactive = 0x2; + static final int ApplicationActive = 0x4; + } + + static class ApplicationStateDetails { + int state = ApplicationState.ApplicationSuspended; + boolean nativePluginIntegrationReady = false; + boolean isStarted = false; } - public static String loadMainLibrary(final String mainLibrary, final String nativeLibraryDir) + static ApplicationStateDetails getStateDetails() { - final String[] res = new String[1]; - res[0] = null; - m_qtThread.run(new Runnable() { - @Override - public void run() { - try { - String mainLibNameTemplate = "lib" + mainLibrary + ".so"; - File f = new File(nativeLibraryDir + mainLibNameTemplate); - if (!f.exists()) { - try { - ApplicationInfo info = getContext().getApplicationContext().getPackageManager() - .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir; - if (info.metaData.containsKey("android.app.system_libs_prefix")) - systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix"); - f = new File(systemLibraryDir + mainLibNameTemplate); - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - if (!f.exists()) - return; - System.load(f.getAbsolutePath()); - res[0] = f.getAbsolutePath(); - } catch (Exception e) { - Log.e(QtTAG, "Can't load '" + mainLibrary + "'", e); - } - } - }); - return res[0]; + return m_stateDetails; } - public static void setActivity(Activity qtMainActivity, QtActivityDelegate qtActivityDelegate) + static void setStarted(boolean started) { - synchronized (m_mainActivityMutex) { - m_activity = qtMainActivity; - m_activityDelegate = qtActivityDelegate; - } + m_stateDetails.isStarted = started; + notifyAppStateDetailsChanged(m_stateDetails); } - public static void setService(Service qtMainService, QtServiceDelegate qtServiceDelegate) + @UsedFromNativeCode + static void notifyNativePluginIntegrationReady(boolean ready) { - synchronized (m_mainActivityMutex) { - m_service = qtMainService; - m_serviceDelegate = qtServiceDelegate; - } + m_stateDetails.nativePluginIntegrationReady = ready; + notifyNativePluginIntegrationReadyChanged(ready); + notifyAppStateDetailsChanged(m_stateDetails); } - public static void setApplicationState(int state) + static void setApplicationState(int state) { synchronized (m_mainActivityMutex) { - switch (state) { - case QtActivityDelegate.ApplicationActive: - m_activityPaused = false; - Iterator<Runnable> itr = m_lostActions.iterator(); - while (itr.hasNext()) - runAction(itr.next()); - m_lostActions.clear(); - break; - default: - m_activityPaused = true; - break; + m_stateDetails.state = state; + if (state == ApplicationState.ApplicationActive) { + for (Runnable mLostAction : m_lostActions) + runAction(mLostAction); + m_lostActions.clear(); } } updateApplicationState(state); + notifyAppStateDetailsChanged(m_stateDetails); + } + + static void registerAppStateListener(AppStateDetailsListener listener) { + synchronized (m_appStateListenersLock) { + if (!m_appStateListeners.contains(listener)) + m_appStateListeners.add(listener); + } } - private static void runAction(Runnable action) + static void unregisterAppStateListener(AppStateDetailsListener listener) { + synchronized (m_appStateListenersLock) { + m_appStateListeners.remove(listener); + } + } + + static void notifyNativePluginIntegrationReadyChanged(boolean ready) { + synchronized (m_appStateListenersLock) { + for (final AppStateDetailsListener listener : m_appStateListeners) + listener.onNativePluginIntegrationReadyChanged(ready); + } + } + + static void notifyAppStateDetailsChanged(ApplicationStateDetails details) { + synchronized (m_appStateListenersLock) { + for (AppStateDetailsListener listener : m_appStateListeners) + listener.onAppStateDetailsChanged(details); + } + } + + // Post a runnable to Main (UI) Thread if the app is active, + // otherwise, queue it to be posted when the the app is active again + static void runAction(Runnable action) + { + runAction(action, true); + } + + static void runAction(Runnable action, boolean queueWhenInactive) { synchronized (m_mainActivityMutex) { final Looper mainLooper = Looper.getMainLooper(); final Handler handler = new Handler(mainLooper); - final boolean active = (m_activity != null && !m_activityPaused) || m_service != null; - if (!active || mainLooper == null || !handler.post(action)) - m_lostActions.add(action); + + if (queueWhenInactive) { + final boolean isStateVisible = + (m_stateDetails.state != ApplicationState.ApplicationSuspended) + && (m_stateDetails.state != ApplicationState.ApplicationHidden); + final boolean active = (isActivityValid() && isStateVisible) || isServiceValid(); + if (!active || !handler.post(action)) + m_lostActions.add(action); + } else { + handler.post(action); + } } } + @UsedFromNativeCode private static void runPendingCppRunnablesOnAndroidThread() { synchronized (m_mainActivityMutex) { - if (m_activity != null) { - if (!m_activityPaused) - m_activity.runOnUiThread(runPendingCppRunnablesRunnable); + if (isActivityValid()) { + if (m_stateDetails.state == ApplicationState.ApplicationActive) + m_activity.get().runOnUiThread(runPendingCppRunnablesRunnable); else runAction(runPendingCppRunnablesRunnable); } else { @@ -379,662 +323,54 @@ public class QtNative } } + @UsedFromNativeCode private static void setViewVisibility(final View view, final boolean visible) { - runAction(new Runnable() { - @Override - public void run() { - view.setVisibility(visible ? View.VISIBLE : View.GONE); - } - }); - } - - public static Display getDisplay(int displayId) - { - Context context = getContext(); - DisplayManager displayManager = - (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); - if (displayManager != null) { - return displayManager.getDisplay(displayId); - } - return null; - } - - public static List<Display> getAvailableDisplays() - { - Context context = getContext(); - DisplayManager displayManager = - (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); - if (displayManager != null) { - Display[] displays = displayManager.getDisplays(); - return Arrays.asList(displays); - } - return new ArrayList<Display>(); + runAction(() -> view.setVisibility(visible ? View.VISIBLE : View.GONE)); } - public static boolean startApplication(String params, String mainLib) throws Exception + static void startApplication(String params, String mainLib) { - if (params == null) - params = "-platform\tandroid"; - - final boolean[] res = new boolean[1]; - res[0] = false; synchronized (m_mainActivityMutex) { - if (params.length() > 0 && !params.startsWith("\t")) - params = "\t" + params; - final String qtParams = mainLib + params; - m_qtThread.run(new Runnable() { - @Override - public void run() { - res[0] = startQtAndroidPlugin(qtParams); - setDisplayMetrics( - m_displayMetricsScreenWidthPixels, m_displayMetricsScreenHeightPixels, - m_displayMetricsAvailableLeftPixels, m_displayMetricsAvailableTopPixels, - m_displayMetricsAvailableWidthPixels, - m_displayMetricsAvailableHeightPixels, m_displayMetricsXDpi, - m_displayMetricsYDpi, m_displayMetricsScaledDensity, - m_displayMetricsDensity, m_displayMetricsRefreshRate); - } - }); - m_qtThread.post(new Runnable() { - @Override - public void run() { - startQtApplication(); - } + m_qtThread.run(() -> { + final String qtParams = mainLib + " " + params; + if (!startQtAndroidPlugin(qtParams)) + Log.e(QtTAG, "An error occurred while starting the Qt Android plugin"); }); + m_qtThread.post(QtNative::startQtApplication); waitForServiceSetup(); - m_started = true; - } - return res[0]; - } - - public static void setApplicationDisplayMetrics(int screenWidthPixels, int screenHeightPixels, - int availableLeftPixels, int availableTopPixels, - int availableWidthPixels, - int availableHeightPixels, double XDpi, - double YDpi, double scaledDensity, - double density, float refreshRate) - { - /* Fix buggy dpi report */ - if (XDpi < android.util.DisplayMetrics.DENSITY_LOW) - XDpi = android.util.DisplayMetrics.DENSITY_LOW; - if (YDpi < android.util.DisplayMetrics.DENSITY_LOW) - YDpi = android.util.DisplayMetrics.DENSITY_LOW; - - synchronized (m_mainActivityMutex) { - if (m_started) { - setDisplayMetrics(screenWidthPixels, screenHeightPixels, availableLeftPixels, - availableTopPixels, availableWidthPixels, availableHeightPixels, - XDpi, YDpi, scaledDensity, density, refreshRate); - } else { - m_displayMetricsScreenWidthPixels = screenWidthPixels; - m_displayMetricsScreenHeightPixels = screenHeightPixels; - m_displayMetricsAvailableLeftPixels = availableLeftPixels; - m_displayMetricsAvailableTopPixels = availableTopPixels; - m_displayMetricsAvailableWidthPixels = availableWidthPixels; - m_displayMetricsAvailableHeightPixels = availableHeightPixels; - m_displayMetricsXDpi = XDpi; - m_displayMetricsYDpi = YDpi; - m_displayMetricsScaledDensity = scaledDensity; - m_displayMetricsDensity = density; - m_displayMetricsRefreshRate = refreshRate; - } + m_stateDetails.isStarted = true; + notifyAppStateDetailsChanged(m_stateDetails); } } - - - // application methods - public static native boolean startQtAndroidPlugin(String params); - public static native void startQtApplication(); - public static native void waitForServiceSetup(); - public static native void quitQtCoreApplication(); - public static native void quitQtAndroidPlugin(); - public static native void terminateQt(); - public static native boolean updateNativeActivity(); - // application methods - - public static void quitApp() + static void quitApp() { - runAction(new Runnable() { - @Override - public void run() { - quitQtAndroidPlugin(); - if (m_activity != null) - m_activity.finish(); - if (m_service != null) - m_service.stopSelf(); - - m_started = false; - } + runAction(() -> { + quitQtAndroidPlugin(); + if (isActivityValid()) + m_activity.get().finish(); + if (isServiceValid()) + m_service.get().stopSelf(); + m_stateDetails.isStarted = false; + notifyAppStateDetailsChanged(m_stateDetails); }); } - //@ANDROID-9 - static private int getAction(int index, MotionEvent event) - { - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_MOVE) { - int hsz = event.getHistorySize(); - if (hsz > 0) { - float x = event.getX(index); - float y = event.getY(index); - for (int h = 0; h < hsz; ++h) { - if ( event.getHistoricalX(index, h) != x || - event.getHistoricalY(index, h) != y ) - return 1; - } - return 2; - } - return 1; - } - if (action == MotionEvent.ACTION_DOWN - || action == MotionEvent.ACTION_POINTER_DOWN && index == event.getActionIndex()) { - return 0; - } else if (action == MotionEvent.ACTION_UP - || action == MotionEvent.ACTION_POINTER_UP && index == event.getActionIndex()) { - return 3; - } - return 2; - } - //@ANDROID-9 - - static public void sendTouchEvent(MotionEvent event, int id) - { - int pointerType = 0; - - if (m_tabletEventSupported == null) - m_tabletEventSupported = isTabletEventSupported(); - - switch (event.getToolType(0)) { - case MotionEvent.TOOL_TYPE_STYLUS: - pointerType = 1; // QTabletEvent::Pen - break; - case MotionEvent.TOOL_TYPE_ERASER: - pointerType = 3; // QTabletEvent::Eraser - break; - } - - if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { - sendMouseEvent(event, id); - } else if (m_tabletEventSupported && pointerType != 0) { - tabletEvent(id, event.getDeviceId(), event.getEventTime(), event.getAction(), pointerType, - event.getButtonState(), event.getX(), event.getY(), event.getPressure()); - } else { - touchBegin(id); - for (int i = 0; i < event.getPointerCount(); ++i) { - touchAdd(id, - event.getPointerId(i), - getAction(i, event), - i == 0, - (int)event.getX(i), - (int)event.getY(i), - event.getTouchMajor(i), - event.getTouchMinor(i), - event.getOrientation(i), - event.getPressure(i)); - } - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - touchEnd(id, 0); - break; - - case MotionEvent.ACTION_UP: - touchEnd(id, 2); - break; - - case MotionEvent.ACTION_CANCEL: - touchCancel(id); - break; - - default: - touchEnd(id, 1); - } - } - } - - static public void sendTrackballEvent(MotionEvent event, int id) - { - sendMouseEvent(event,id); - } - - static public boolean sendGenericMotionEvent(MotionEvent event, int id) - { - if (((event.getAction() & (MotionEvent.ACTION_SCROLL | MotionEvent.ACTION_HOVER_MOVE)) == 0) - || (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != InputDevice.SOURCE_CLASS_POINTER) { - return false; - } - - return sendMouseEvent(event, id); - } - - static public boolean sendMouseEvent(MotionEvent event, int id) - { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_UP: - mouseUp(id, (int) event.getX(), (int) event.getY()); - break; - - case MotionEvent.ACTION_DOWN: - mouseDown(id, (int) event.getX(), (int) event.getY()); - m_oldx = (int) event.getX(); - m_oldy = (int) event.getY(); - break; - case MotionEvent.ACTION_HOVER_MOVE: - case MotionEvent.ACTION_MOVE: - if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { - mouseMove(id, (int) event.getX(), (int) event.getY()); - } else { - int dx = (int) (event.getX() - m_oldx); - int dy = (int) (event.getY() - m_oldy); - if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { - mouseMove(id, (int) event.getX(), (int) event.getY()); - m_oldx = (int) event.getX(); - m_oldy = (int) event.getY(); - } - } - break; - case MotionEvent.ACTION_SCROLL: - mouseWheel(id, (int) event.getX(), (int) event.getY(), - event.getAxisValue(MotionEvent.AXIS_HSCROLL), event.getAxisValue(MotionEvent.AXIS_VSCROLL)); - break; - default: - return false; - } - return true; - } - - public static Context getContext() { - if (m_activity != null) - return m_activity; - return m_service; - } - - public static int checkSelfPermission(String permission) + @UsedFromNativeCode + static int checkSelfPermission(String permission) { - int perm = PackageManager.PERMISSION_DENIED; synchronized (m_mainActivityMutex) { Context context = getContext(); PackageManager pm = context.getPackageManager(); - perm = pm.checkPermission(permission, context.getPackageName()); - } - - return perm; - } - - private static void updateSelection(final int selStart, - final int selEnd, - final int candidatesStart, - final int candidatesEnd) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.updateSelection(selStart, selEnd, candidatesStart, candidatesEnd); - } - }); - } - - private static int getSelectHandleWidth() - { - return m_activityDelegate.getSelectHandleWidth(); - } - - private static void updateHandles(final int mode, - final int editX, - final int editY, - final int editButtons, - final int x1, - final int y1, - final int x2, - final int y2, - final boolean rtl) - { - runAction(new Runnable() { - @Override - public void run() { - m_activityDelegate.updateHandles(mode, editX, editY, editButtons, x1, y1, x2, y2, rtl); - } - }); - } - - private static void showSoftwareKeyboard(final int x, - final int y, - final int width, - final int height, - final int inputHints, - final int enterKeyType) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.showSoftwareKeyboard(x, y, width, height, inputHints, enterKeyType); - } - }); - } - - private static void resetSoftwareKeyboard() - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.resetSoftwareKeyboard(); - } - }); - } - - private static void hideSoftwareKeyboard() - { - m_isKeyboardHiding = true; - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.hideSoftwareKeyboard(); - } - }); - } - - private static void setSystemUiVisibility(final int systemUiVisibility) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.setSystemUiVisibility(systemUiVisibility); - } - updateWindow(); - } - }); - } - - public static boolean isSoftwareKeyboardVisible() - { - return m_activityDelegate.isKeyboardVisible() && !m_isKeyboardHiding; - } - - private static void notifyAccessibilityLocationChange(final int viewId) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.notifyAccessibilityLocationChange(viewId); - } - } - }); - } - - private static void notifyObjectHide(final int viewId, final int parentId) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.notifyObjectHide(viewId, parentId); - } - } - }); - } - - private static void notifyObjectFocus(final int viewId) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.notifyObjectFocus(viewId); - } - } - }); - } - - private static void notifyValueChanged(int viewId, String value) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.notifyValueChanged(viewId, value); - } - } - }); - } - - private static void notifyScrolledEvent(final int viewId) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) { - m_activityDelegate.notifyScrolledEvent(viewId); - } - } - }); - } - - public static void notifyQtAndroidPluginRunning(final boolean running) - { - m_activityDelegate.notifyQtAndroidPluginRunning(running); - } - - private static void registerClipboardManager() - { - if (m_service == null || m_activity != null) { // Avoid freezing if only service - final Semaphore semaphore = new Semaphore(0); - runAction(new Runnable() { - @Override - public void run() { - if (m_activity != null) - m_clipboardManager = (android.content.ClipboardManager) m_activity.getSystemService(Context.CLIPBOARD_SERVICE); - if (m_clipboardManager != null) { - m_clipboardManager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() { - public void onPrimaryClipChanged() { - onClipboardDataChanged(); - } - }); - } - semaphore.release(); - } - }); - try { - semaphore.acquire(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private static void clearClipData() - { - if (m_clipboardManager != null) { - if (Build.VERSION.SDK_INT >= 28) { - m_clipboardManager.clearPrimaryClip(); - } else { - String[] mimeTypes = { ClipDescription.MIMETYPE_UNKNOWN }; - ClipData data = new ClipData("", mimeTypes, new ClipData.Item(new Intent())); - m_clipboardManager.setPrimaryClip(data); - } - } - m_usePrimaryClip = false; - } - private static void setClipboardText(String text) - { - if (m_clipboardManager != null) { - ClipData clipData = ClipData.newPlainText("text/plain", text); - updatePrimaryClip(clipData); - } - } - - public static boolean hasClipboardText() - { - return hasClipboardMimeType("text/plain"); - } - - private static String getClipboardText() - { - try { - if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { - ClipData primaryClip = m_clipboardManager.getPrimaryClip(); - for (int i = 0; i < primaryClip.getItemCount(); ++i) - if (primaryClip.getItemAt(i).getText() != null) - return primaryClip.getItemAt(i).getText().toString(); - } - } catch (Exception e) { - Log.e(QtTAG, "Failed to get clipboard data", e); - } - return ""; - } - - private static void updatePrimaryClip(ClipData clipData) - { - try { - if (m_usePrimaryClip) { - ClipData clip = m_clipboardManager.getPrimaryClip(); - if (Build.VERSION.SDK_INT >= 26) { - Objects.requireNonNull(clip).addItem(m_activity.getContentResolver(), clipData.getItemAt(0)); - } else { - Objects.requireNonNull(clip).addItem(clipData.getItemAt(0)); - } - m_clipboardManager.setPrimaryClip(clip); - } else { - m_clipboardManager.setPrimaryClip(clipData); - m_usePrimaryClip = true; - } - } catch (Exception e) { - Log.e(QtTAG, "Failed to set clipboard data", e); - } - } - - private static void setClipboardHtml(String text, String html) - { - if (m_clipboardManager != null) { - ClipData clipData = ClipData.newHtmlText("text/html", text, html); - updatePrimaryClip(clipData); - } - } - - private static boolean hasClipboardMimeType(String mimeType) - { - if (m_clipboardManager == null) - return false; - - ClipDescription description = m_clipboardManager.getPrimaryClipDescription(); - // getPrimaryClipDescription can fail if the app does not have input focus - if (description == null) - return false; - - for (int i = 0; i < description.getMimeTypeCount(); ++i) { - String itemMimeType = description.getMimeType(i); - if (itemMimeType.equals(mimeType)) - return true; - } - return false; - } - - public static boolean hasClipboardHtml() - { - return hasClipboardMimeType("text/html"); - } - - private static String getClipboardHtml() - { - try { - if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { - ClipData primaryClip = m_clipboardManager.getPrimaryClip(); - for (int i = 0; i < primaryClip.getItemCount(); ++i) - if (primaryClip.getItemAt(i).getHtmlText() != null) - return primaryClip.getItemAt(i).getHtmlText().toString(); - } - } catch (Exception e) { - Log.e(QtTAG, "Failed to get clipboard data", e); - } - return ""; - } - - private static void setClipboardUri(String uriString) - { - if (m_clipboardManager != null) { - ClipData clipData = ClipData.newUri(m_activity.getContentResolver(), "text/uri-list", - Uri.parse(uriString)); - updatePrimaryClip(clipData); - } - } - - public static boolean hasClipboardUri() - { - return hasClipboardMimeType("text/uri-list"); - } - - private static String[] getClipboardUris() - { - ArrayList<String> uris = new ArrayList<String>(); - try { - if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) { - ClipData primaryClip = m_clipboardManager.getPrimaryClip(); - for (int i = 0; i < primaryClip.getItemCount(); ++i) - if (primaryClip.getItemAt(i).getUri() != null) - uris.add(primaryClip.getItemAt(i).getUri().toString()); - } - } catch (Exception e) { - Log.e(QtTAG, "Failed to get clipboard data", e); + return pm.checkPermission(permission, context.getPackageName()); } - String[] strings = new String[uris.size()]; - strings = uris.toArray(strings); - return strings; - } - - private static void openContextMenu(final int x, final int y, final int w, final int h) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.openContextMenu(x, y, w, h); - } - }); - } - - private static void closeContextMenu() - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.closeContextMenu(); - } - }); - } - - private static void resetOptionsMenu() - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.resetOptionsMenu(); - } - }); - } - - private static void openOptionsMenu() - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activity != null) - m_activity.openOptionsMenu(); - } - }); } + @UsedFromNativeCode private static byte[][] getSSLCertificates() { - ArrayList<byte[]> certificateList = new ArrayList<byte[]>(); + ArrayList<byte[]> certificateList = new ArrayList<>(); try { TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); @@ -1045,7 +381,7 @@ public class QtNative X509TrustManager trustManager = (X509TrustManager) manager; for (X509Certificate certificate : trustManager.getAcceptedIssuers()) { - byte buffer[] = certificate.getEncoded(); + byte[] buffer = certificate.getEncoded(); certificateList.add(buffer); } } @@ -1059,105 +395,13 @@ public class QtNative return certificateArray; } - private static void createSurface(final int id, final boolean onTop, final int x, final int y, final int w, final int h, final int imageDepth) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.createSurface(id, onTop, x, y, w, h, imageDepth); - } - }); - } - - private static void insertNativeView(final int id, final View view, final int x, final int y, final int w, final int h) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.insertNativeView(id, view, x, y, w, h); - } - }); - } - - private static void setSurfaceGeometry(final int id, final int x, final int y, final int w, final int h) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.setSurfaceGeometry(id, x, y, w, h); - } - }); - } - - private static void bringChildToFront(final int id) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.bringChildToFront(id); - } - }); - } - - private static void bringChildToBack(final int id) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.bringChildToBack(id); - } - }); - } - - private static void destroySurface(final int id) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.destroySurface(id); - } - }); - } - - private static void initializeAccessibility() - { - runAction(new Runnable() { - @Override - public void run() { - m_activityDelegate.initializeAccessibility(); - } - }); - } - - private static void hideSplashScreen(final int duration) - { - runAction(new Runnable() { - @Override - public void run() { - if (m_activityDelegate != null) - m_activityDelegate.hideSplashScreen(duration); - } - }); - } - - public static void keyboardVisibilityUpdated(boolean visibility) - { - m_isKeyboardHiding = false; - keyboardVisibilityChanged(visibility); - } - + @UsedFromNativeCode private static String[] listAssetContent(android.content.res.AssetManager asset, String path) { String [] list; - ArrayList<String> res = new ArrayList<String>(); + ArrayList<String> res = new ArrayList<>(); try { list = asset.list(path); - if (list.length > 0) { + if (list != null) { for (String file : list) { try { String[] isDir = asset.list(path.length() > 0 ? path + "/" + file : file); @@ -1172,129 +416,47 @@ public class QtNative } catch (Exception e) { e.printStackTrace(); } - return res.toArray(new String[res.size()]); - } - - /** - *Sets a single environment variable - * - * returns true if the value was set, false otherwise. - * in case it cannot set value will log the exception - **/ - public static void setEnvironmentVariable(String key, String value) - { - try { - android.system.Os.setenv(key, value, true); - } catch (Exception e) { - Log.e(QtNative.QtTAG, "Could not set environment variable:" + key + "=" + value); - e.printStackTrace(); - } + return res.toArray(new String[0]); } - /** - *Sets multiple environment variables - * - * Uses '\t' as divider between variables and '=' between key/value - * Ex: key1=val1\tkey2=val2\tkey3=val3 - * Note: it assumed that the key cannot have '=' but the value can - **/ - public static void setEnvironmentVariables(String environmentVariables) - { - for (String variable : environmentVariables.split("\t")) { - String[] keyvalue = variable.split("=", 2); - if (keyvalue.length < 2 || keyvalue[0].isEmpty()) - continue; - - setEnvironmentVariable(keyvalue[0], keyvalue[1]); - } - } - - // screen methods - public static native void setDisplayMetrics(int screenWidthPixels, int screenHeightPixels, - int availableLeftPixels, int availableTopPixels, - int availableWidthPixels, int availableHeightPixels, - double XDpi, double YDpi, double scaledDensity, - double density, float refreshRate); - public static native void handleOrientationChanged(int newRotation, int nativeOrientation); - public static native void handleRefreshRateChanged(float refreshRate); - public static native void handleScreenAdded(int displayId); - public static native void handleScreenChanged(int displayId); - public static native void handleScreenRemoved(int displayId); - // screen methods - public static native void handleUiDarkModeChanged(int newUiMode); - - // pointer methods - public static native void mouseDown(int winId, int x, int y); - public static native void mouseUp(int winId, int x, int y); - public static native void mouseMove(int winId, int x, int y); - public static native void mouseWheel(int winId, int x, int y, float hdelta, float vdelta); - public static native void touchBegin(int winId); - public static native void touchAdd(int winId, int pointerId, int action, boolean primary, int x, int y, float major, float minor, float rotation, float pressure); - public static native void touchEnd(int winId, int action); - public static native void touchCancel(int winId); - public static native void longPress(int winId, int x, int y); - // pointer methods - - // tablet methods - public static native boolean isTabletEventSupported(); - public static native void tabletEvent(int winId, int deviceId, long time, int action, int pointerType, int buttonState, float x, float y, float pressure); - // tablet methods - - // keyboard methods - public static native void keyDown(int key, int unicode, int modifier, boolean autoRepeat); - public static native void keyUp(int key, int unicode, int modifier, boolean autoRepeat); - public static native void keyboardVisibilityChanged(boolean visibility); - public static native void keyboardGeometryChanged(int x, int y, int width, int height); - // keyboard methods - - // handle methods - public static final int IdCursorHandle = 1; - public static final int IdLeftHandle = 2; - public static final int IdRightHandle = 3; - public static native void handleLocationChanged(int id, int x, int y); - // handle methods - - // dispatch events methods - public static native boolean dispatchGenericMotionEvent(MotionEvent ev); - public static native boolean dispatchKeyEvent(KeyEvent event); - // dispatch events methods - - // surface methods - public static native void setSurface(int id, Object surface, int w, int h); - // surface methods + // application methods + static native boolean startQtAndroidPlugin(String params); + static native void startQtApplication(); + static native void waitForServiceSetup(); + static native void quitQtCoreApplication(); + static native void quitQtAndroidPlugin(); + static native void terminateQt(); + static native boolean updateNativeActivity(); + // application methods // window methods - public static native void updateWindow(); + static native void updateWindow(); // window methods // application methods - public static native void updateApplicationState(int state); + static native void updateApplicationState(int state); // menu methods - public static native boolean onPrepareOptionsMenu(Menu menu); - public static native boolean onOptionsItemSelected(int itemId, boolean checked); - public static native void onOptionsMenuClosed(Menu menu); - - public static native void onCreateContextMenu(ContextMenu menu); - public static native void fillContextMenu(Menu menu); - public static native boolean onContextItemSelected(int itemId, boolean checked); - public static native void onContextMenuClosed(Menu menu); + static native boolean onPrepareOptionsMenu(Menu menu); + static native boolean onOptionsItemSelected(int itemId, boolean checked); + static native void onOptionsMenuClosed(Menu menu); + + static native void onCreateContextMenu(ContextMenu menu); + static native void fillContextMenu(Menu menu); + static native boolean onContextItemSelected(int itemId, boolean checked); + static native void onContextMenuClosed(Menu menu); // menu methods - // clipboard methods - public static native void onClipboardDataChanged(); - // clipboard methods - // activity methods - public static native void onActivityResult(int requestCode, int resultCode, Intent data); + static native void onActivityResult(int requestCode, int resultCode, Intent data); public static native void onNewIntent(Intent data); - public static native void runPendingCppRunnables(); + static native void runPendingCppRunnables(); - public static native void sendRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); + static native void sendRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); // activity methods // service methods - public static native IBinder onBind(Intent intent); + static native IBinder onBind(Intent intent); // service methods } diff --git a/src/android/jar/src/org/qtproject/qt/android/accessibility/QtNativeAccessibility.java b/src/android/jar/src/org/qtproject/qt/android/QtNativeAccessibility.java index d4cce935ba..dd2cead8cd 100644 --- a/src/android/jar/src/org/qtproject/qt/android/accessibility/QtNativeAccessibility.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtNativeAccessibility.java @@ -1,7 +1,7 @@ // Copyright (C) 2016 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.accessibility; +package org.qtproject.qt.android; import android.graphics.Rect; import android.view.accessibility.AccessibilityNodeInfo; @@ -19,5 +19,4 @@ class QtNativeAccessibility static native boolean scrollBackward(int objectId); static native boolean populateNode(int objectId, AccessibilityNodeInfo node); - static native String valueForAccessibleObject(int objectId); } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtNativeLibrariesDir.java b/src/android/jar/src/org/qtproject/qt/android/QtNativeLibrariesDir.java deleted file mode 100644 index b857e7b607..0000000000 --- a/src/android/jar/src/org/qtproject/qt/android/QtNativeLibrariesDir.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> -// 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.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager.NameNotFoundException; - -public class QtNativeLibrariesDir { - public static final String systemLibrariesDir = "/system/lib/"; - public static String nativeLibrariesDir(Context context) - { - String m_nativeLibraryDir = null; - try { - ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); - m_nativeLibraryDir = ai.nativeLibraryDir + "/"; - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - return m_nativeLibraryDir; - } -} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtRootLayout.java b/src/android/jar/src/org/qtproject/qt/android/QtRootLayout.java new file mode 100644 index 0000000000..b8743d5ce0 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtRootLayout.java @@ -0,0 +1,61 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> +// 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.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.content.res.Configuration; +import android.view.Surface; + +/** + A layout which corresponds to one Activity, i.e. is the root layout where the top level window + and handles orientation changes. +*/ +class QtRootLayout extends QtLayout +{ + private int m_previousRotation = -1; + + public QtRootLayout(Context context) + { + super(context); + } + + @Override + protected void onSizeChanged (int w, int h, int oldw, int oldh) + { + Activity activity = (Activity)getContext(); + if (activity == null) + return; + + QtDisplayManager.setApplicationDisplayMetrics(activity, w, h); + QtDisplayManager.handleOrientationChanges(activity); + } + + @Override + public void onConfigurationChanged(Configuration configuration) + { + Context context = getContext(); + if (context instanceof Activity) { + Activity activity = (Activity)context; + //if orientation change is betwen invertedPortrait and portrait or + //invertedLandscape and landscape, we do not get sizeChanged callback. + int rotation = QtDisplayManager.getDisplayRotation(activity); + if (isSameSizeForOrientations(rotation, m_previousRotation)) + QtDisplayManager.handleOrientationChanges(activity); + m_previousRotation = rotation; + } + } + + public boolean isSameSizeForOrientations(int r1, int r2) { + return (r1 == r2) || + (r1 == Surface.ROTATION_0 && r2 == Surface.ROTATION_180) + || (r1 == Surface.ROTATION_180 && r2 == Surface.ROTATION_0) + || (r1 == Surface.ROTATION_90 && r2 == Surface.ROTATION_270) + || (r1 == Surface.ROTATION_270 && r2 == Surface.ROTATION_90); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java new file mode 100644 index 0000000000..b69dea4416 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java @@ -0,0 +1,51 @@ +// 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 + +package org.qtproject.qt.android; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +public class QtServiceBase extends Service { + @Override + public void onCreate() + { + super.onCreate(); + + // the application has already started, do not reload everything again + if (QtNative.getStateDetails().isStarted) { + Log.w(QtNative.QtTAG, + "A QtService tried to start in the same process as an initiated " + + "QtActivity. That is not supported. This results in the service " + + "functioning as an Android Service detached from Qt."); + return; + } + + QtNative.setService(this); + + QtServiceLoader loader = new QtServiceLoader(this); + loader.loadQtLibraries(); + QtNative.startApplication(loader.getApplicationParameters(), loader.getMainLibraryPath()); + QtNative.setApplicationState(QtNative.ApplicationState.ApplicationHidden); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + QtNative.quitQtCoreApplication(); + QtNative.terminateQt(); + QtNative.setService(null); + QtNative.getQtThread().exit(); + System.exit(0); + } + + @Override + public IBinder onBind(Intent intent) { + synchronized (this) { + return QtNative.onBind(intent); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java deleted file mode 100644 index 4836985cfc..0000000000 --- a/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2016 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 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.Service; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; -import android.graphics.drawable.ColorDrawable; -import android.net.LocalServerSocket; -import android.net.LocalSocket; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.ResultReceiver; -import android.text.method.MetaKeyKeyListener; -import android.util.Base64; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.Surface; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; - -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileWriter; -import java.io.InputStreamReader; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Objects; - -public class QtServiceDelegate -{ - private static final String NATIVE_LIBRARIES_KEY = "native.libraries"; - private static final String BUNDLED_LIBRARIES_KEY = "bundled.libraries"; - private static final String MAIN_LIBRARY_KEY = "main.library"; - private static final String ENVIRONMENT_VARIABLES_KEY = "environment.variables"; - private static final String APPLICATION_PARAMETERS_KEY = "application.parameters"; - private static final String STATIC_INIT_CLASSES_KEY = "static.init.classes"; - private static final String APP_DISPLAY_METRIC_SCREEN_DESKTOP_KEY = "display.screen.desktop"; - private static final String APP_DISPLAY_METRIC_SCREEN_XDPI_KEY = "display.screen.dpi.x"; - private static final String APP_DISPLAY_METRIC_SCREEN_YDPI_KEY = "display.screen.dpi.y"; - private static final String APP_DISPLAY_METRIC_SCREEN_DENSITY_KEY = "display.screen.density"; - - private String m_mainLib = null; - private Service m_service = null; - private static String m_applicationParameters = null; - - public boolean loadApplication(Service service, ClassLoader classLoader, Bundle loaderParams) - { - /// check parameters integrity - if (!loaderParams.containsKey(NATIVE_LIBRARIES_KEY) - || !loaderParams.containsKey(BUNDLED_LIBRARIES_KEY)) { - return false; - } - - m_service = service; - QtNative.setService(m_service, this); - QtNative.setClassLoader(classLoader); - - QtNative.setApplicationDisplayMetrics(10, 10, 0, 0, 10, 10, 120, 120, 1.0, 1.0, 60.0f); - - if (loaderParams.containsKey(STATIC_INIT_CLASSES_KEY)) { - for (String className : - Objects.requireNonNull(loaderParams.getStringArray(STATIC_INIT_CLASSES_KEY))) { - if (className.length() == 0) - continue; - try { - Class<?> initClass = classLoader.loadClass(className); - Object staticInitDataObject = initClass.newInstance(); // create an instance - try { - Method m = initClass.getMethod("setService", Service.class, Object.class); - m.invoke(staticInitDataObject, m_service, this); - } catch (Exception e) { - Log.d(QtNative.QtTAG, - "Class " + className + " does not implement setService method"); - } - - // For modules that don't need/have setService - try { - Method m = initClass.getMethod("setContext", Context.class); - m.invoke(staticInitDataObject, (Context)m_service); - } catch (Exception e) { - e.printStackTrace(); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - QtNative.loadQtLibraries(loaderParams.getStringArrayList(NATIVE_LIBRARIES_KEY)); - ArrayList<String> libraries = loaderParams.getStringArrayList(BUNDLED_LIBRARIES_KEY); - String nativeLibsDir = QtNativeLibrariesDir.nativeLibrariesDir(m_service); - QtNative.loadBundledLibraries(libraries, nativeLibsDir); - m_mainLib = loaderParams.getString(MAIN_LIBRARY_KEY); - - QtNative.setEnvironmentVariables(loaderParams.getString(ENVIRONMENT_VARIABLES_KEY)); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", - "Droid Sans Mono;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif"); - QtNative.setEnvironmentVariable("HOME", m_service.getFilesDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("TMPDIR", m_service.getCacheDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS", - "Roboto;Droid Sans;Droid Sans Fallback"); - - if (loaderParams.containsKey(APPLICATION_PARAMETERS_KEY)) - m_applicationParameters = loaderParams.getString(APPLICATION_PARAMETERS_KEY); - else - m_applicationParameters = ""; - - m_mainLib = QtNative.loadMainLibrary(m_mainLib, nativeLibsDir); - return m_mainLib != null; - } - - public boolean startApplication() - { - // start application - try { - String nativeLibraryDir = QtNativeLibrariesDir.nativeLibrariesDir(m_service); - QtNative.startApplication(m_applicationParameters, m_mainLib); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public void onDestroy() - { - QtNative.quitQtCoreApplication(); - QtNative.terminateQt(); - QtNative.setService(null, null); - QtNative.m_qtThread.exit(); - System.exit(0); - } - - public IBinder onBind(Intent intent) - { - synchronized (this) { - return QtNative.onBind(intent); - } - } -} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceEmbeddedDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceEmbeddedDelegate.java new file mode 100644 index 0000000000..d8af626ca0 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceEmbeddedDelegate.java @@ -0,0 +1,111 @@ +// 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 static org.qtproject.qt.android.QtNative.ApplicationState.ApplicationSuspended; + +import android.app.Service; +import android.content.Context; +import android.content.res.Resources; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.View; +import android.util.DisplayMetrics; + +/** + * QtServiceEmbeddedDelegate is used for embedding QML into Android Service contexts. Implements + * {@link QtEmbeddedViewInterface} so it can be used by QtView to communicate with the Qt layer. + */ +class QtServiceEmbeddedDelegate implements QtEmbeddedViewInterface, QtNative.AppStateDetailsListener +{ + private final Service m_service; + private QtView m_view; + private boolean m_windowLoaded = false; + + QtServiceEmbeddedDelegate(Service service) + { + m_service = service; + QtNative.registerAppStateListener(this); + QtNative.setService(service); + // QTBUG-122920 TODO Implement accessibility for service UIs + // QTBUG-122552 TODO Implement text input + } + + @Override + public void onNativePluginIntegrationReadyChanged(boolean ready) + { + synchronized (this) { + if (ready) { + QtNative.runAction(() -> { + if (m_view == null) + return; + + final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + + final int maxWidth = m_view.getWidth(); + final int maxHeight = m_view.getHeight(); + final int width = maxWidth; + final int height = maxHeight; + final int insetLeft = m_view.getLeft(); + final int insetTop = m_view.getTop(); + + final DisplayManager dm = m_service.getSystemService(DisplayManager.class); + QtDisplayManager.setDisplayMetrics( + maxWidth, maxHeight, insetLeft, insetTop, width, height, + QtDisplayManager.getXDpi(metrics), QtDisplayManager.getYDpi(metrics), + metrics.scaledDensity, metrics.density, + QtDisplayManager.getRefreshRate( + dm.getDisplay(Display.DEFAULT_DISPLAY))); + }); + createRootWindow(); + } + } + } + + // QtEmbeddedViewInterface implementation begin + @Override + public void startQtApplication(String appParams, String mainLib) + { + QtNative.startApplication(appParams, mainLib); + } + + @Override + public void setView(QtView view) + { + m_view = view; + // If the embedded view is destroyed, do cleanup: + if (view == null) + cleanup(); + } + + @Override + public void queueLoadWindow() + { + synchronized (this) { + if (QtNative.getStateDetails().nativePluginIntegrationReady) + createRootWindow(); + } + } + // QtEmbeddedViewInterface implementation end + + private void createRootWindow() + { + if (m_view != null && !m_windowLoaded) { + QtView.createRootWindow(m_view, m_view.getLeft(), m_view.getTop(), m_view.getWidth(), + m_view.getHeight()); + m_windowLoaded = true; + } + } + + private void cleanup() + { + QtNative.setApplicationState(ApplicationSuspended); + QtNative.unregisterAppStateListener(QtServiceEmbeddedDelegate.this); + QtEmbeddedViewInterfaceFactory.remove(m_service); + + QtNative.terminateQt(); + QtNative.setService(null); + QtNative.getQtThread().exit(); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java new file mode 100644 index 0000000000..ce74ec21e7 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java @@ -0,0 +1,28 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (c) 2016, BogDan Vatra <bogdan@kde.org> +// 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.Service; +import android.content.ContextWrapper; +import android.util.Log; + +class QtServiceLoader extends QtLoader { + private final Service m_service; + + public QtServiceLoader(Service service) { + super(new ContextWrapper(service)); + m_service = service; + + extractContextMetaData(); + } + + @Override + protected void finish() { + if (m_service != null) + m_service.stopSelf(); + else + Log.w(QtTAG, "finish() called when service object is null"); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtSurface.java b/src/android/jar/src/org/qtproject/qt/android/QtSurface.java index 42830783e7..e20974eeac 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtSurface.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtSurface.java @@ -4,42 +4,30 @@ package org.qtproject.qt.android; -import android.app.Activity; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; -import android.view.GestureDetector; -import android.view.MotionEvent; +import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; - -public class QtSurface extends SurfaceView implements SurfaceHolder.Callback +@SuppressLint("ViewConstructor") +class QtSurface extends SurfaceView implements SurfaceHolder.Callback { - private GestureDetector m_gestureDetector; - private Object m_accessibilityDelegate = null; + private final QtSurfaceInterface m_surfaceCallback; - public QtSurface(Context context, int id, boolean onTop, int imageDepth) + public QtSurface(Context context, QtSurfaceInterface surfaceCallback, boolean onTop, int imageDepth) { super(context); setFocusable(false); setFocusableInTouchMode(false); setZOrderMediaOverlay(onTop); + m_surfaceCallback = surfaceCallback; getHolder().addCallback(this); if (imageDepth == 16) getHolder().setFormat(PixelFormat.RGB_565); else getHolder().setFormat(PixelFormat.RGBA_8888); - - setId(id); - m_gestureDetector = - new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { - public void onLongPress(MotionEvent event) { - QtNative.longPress(getId(), (int) event.getX(), (int) event.getY()); - } - }); - m_gestureDetector.setIsLongpressEnabled(true); } @Override @@ -52,39 +40,14 @@ public class QtSurface extends SurfaceView implements SurfaceHolder.Callback { if (width < 1 || height < 1) return; - - QtNative.setSurface(getId(), holder.getSurface(), width, height); + if (m_surfaceCallback != null) + m_surfaceCallback.onSurfaceChanged(holder.getSurface()); } @Override public void surfaceDestroyed(SurfaceHolder holder) { - QtNative.setSurface(getId(), null, 0, 0); - } - - @Override - public boolean onTouchEvent(MotionEvent event) - { - // QTBUG-65927 - // Fix event positions depending on Surface position. - // In case when Surface is moved, we should also add this move to event position - event.setLocation(event.getX() + getX(), event.getY() + getY()); - - QtNative.sendTouchEvent(event, getId()); - m_gestureDetector.onTouchEvent(event); - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent event) - { - QtNative.sendTrackballEvent(event, getId()); - return true; - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) - { - return QtNative.sendGenericMotionEvent(event, getId()); + if (m_surfaceCallback != null) + m_surfaceCallback.onSurfaceChanged(null); } } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtSurfaceInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtSurfaceInterface.java new file mode 100644 index 0000000000..5850f2e3a1 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtSurfaceInterface.java @@ -0,0 +1,12 @@ +// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> +// Copyright (C) 2016 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.view.Surface; + +interface QtSurfaceInterface +{ + void onSurfaceChanged(Surface surface); +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtTextureView.java b/src/android/jar/src/org/qtproject/qt/android/QtTextureView.java new file mode 100644 index 0000000000..95370f3e4b --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtTextureView.java @@ -0,0 +1,51 @@ +// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> +// Copyright (C) 2016 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.content.Context; +import android.graphics.SurfaceTexture; +import android.util.Log; +import android.view.Surface; +import android.view.TextureView; + +class QtTextureView extends TextureView implements TextureView.SurfaceTextureListener +{ + private final QtSurfaceInterface m_surfaceCallback; + private boolean m_staysOnTop; + private Surface m_surface; + + public QtTextureView(Context context, QtSurfaceInterface surfaceCallback, boolean isOpaque) + { + super(context); + setFocusable(false); + setFocusableInTouchMode(false); + m_surfaceCallback = surfaceCallback; + setSurfaceTextureListener(this); + setOpaque(isOpaque); + setSurfaceTexture(new SurfaceTexture(false)); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { + m_surface = new Surface(surfaceTexture); + m_surfaceCallback.onSurfaceChanged(m_surface); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + m_surface = new Surface(surfaceTexture); + m_surfaceCallback.onSurfaceChanged(m_surface); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + m_surfaceCallback.onSurfaceChanged(null); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtThread.java b/src/android/jar/src/org/qtproject/qt/android/QtThread.java index bc58344c16..0943ad3265 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtThread.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtThread.java @@ -6,10 +6,10 @@ package org.qtproject.qt.android; import java.util.ArrayList; import java.util.concurrent.Semaphore; -public class QtThread { - private ArrayList<Runnable> m_pendingRunnables = new ArrayList<Runnable>(); +class QtThread { + private final ArrayList<Runnable> m_pendingRunnables = new ArrayList<>(); private boolean m_exit = false; - private Thread m_qtThread = new Thread(new Runnable() { + private final Thread m_qtThread = new Thread(new Runnable() { @Override public void run() { while (!m_exit) { @@ -18,7 +18,7 @@ public class QtThread { synchronized (m_qtThread) { if (m_pendingRunnables.size() == 0) m_qtThread.wait(); - pendingRunnables = new ArrayList<Runnable>(m_pendingRunnables); + pendingRunnables = new ArrayList<>(m_pendingRunnables); m_pendingRunnables.clear(); } for (Runnable runnable : pendingRunnables) @@ -42,15 +42,20 @@ public class QtThread { } } + public void sleep(int milliseconds) { + try { + m_qtThread.sleep(milliseconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + public void run(final Runnable runnable) { final Semaphore sem = new Semaphore(0); synchronized (m_qtThread) { - m_pendingRunnables.add(new Runnable() { - @Override - public void run() { - runnable.run(); - sem.release(); - } + m_pendingRunnables.add(() -> { + runnable.run(); + sem.release(); }); m_qtThread.notify(); } 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..b4fa0382ed --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtView.java @@ -0,0 +1,202 @@ +// 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; + +// Base class for embedding QWindow into native Android view hierarchy. Extend to implement +// the creation of appropriate window to embed. +abstract class QtView extends ViewGroup { + 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 long m_parentWindowReference; + protected QtWindowListener m_windowListener; + protected QtEmbeddedViewInterface m_viewInterface; + // 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); + + static native void createRootWindow(View rootView, int x, int y, int width, int height); + static native void deleteWindow(long windowReference); + private static native void setWindowVisible(long windowReference, boolean visible); + private static native void resizeWindow(long windowReference, + int x, int y, int width, int height); + + /** + * Create a QtView for embedding a QWindow without loading the Qt libraries or starting + * the Qt app. + * @param context the hosting Context + **/ + public QtView(Context context) { + super(context); + + m_viewInterface = QtEmbeddedViewInterfaceFactory.create(context); + addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (m_windowReference != 0L) { + final int oldWidth = oldRight - oldLeft; + final int oldHeight = oldBottom - oldTop; + final int newWidth = right - left; + final int newHeight = bottom - top; + if (oldWidth != newWidth || oldHeight != newHeight || left != oldLeft || + top != oldTop) { + resizeWindow(m_windowReference, left, top, newWidth, newHeight); + } + } + } + }); + } + /** + * Create a QtView for embedding a QWindow, and 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 { + this(context); + if (appLibName == null || appLibName.isEmpty()) { + throw new InvalidParameterException("QtView: argument 'appLibName' may not be empty "+ + "or null"); + } + + loadQtLibraries(appLibName); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + m_viewInterface.setView(this); + m_viewInterface.queueLoadWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + destroyWindow(); + m_viewInterface.setView(null); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + if (m_window != null) + m_window.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + measureChildren(widthMeasureSpec, heightMeasureSpec); + + final int count = getChildCount(); + + int maxHeight = 0; + int maxWidth = 0; + + // Find out how big everyone wants to be + measureChildren(widthMeasureSpec, heightMeasureSpec); + + // Find rightmost and bottom-most child + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); + } + } + + // Check against minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + + public void setQtWindowListener(QtWindowListener listener) { + m_windowListener = listener; + } + + void loadQtLibraries(String appLibName) { + QtEmbeddedLoader loader = new QtEmbeddedLoader(getContext()); + loader.setMainLibraryName(appLibName); + loader.loadQtLibraries(); + // Start Native Qt application + m_viewInterface.startQtApplication(loader.getApplicationParameters(), + loader.getMainLibraryPath()); + } + + 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_parentWindowReference = parentWindowRef; + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + m_window = window; + m_window.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + addView(m_window, 0); + // Call show window + parent + setWindowVisible(true); + if (m_windowListener != null) + m_windowListener.onQtWindowLoaded(); + } + }); + } + + // Destroy the underlying QWindow + void destroyWindow() { + if (m_parentWindowReference != 0L) + deleteWindow(m_parentWindowReference); + m_parentWindowReference = 0L; + } + + QtWindow getQtWindow() { + return m_window; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtWindow.java b/src/android/jar/src/org/qtproject/qt/android/QtWindow.java new file mode 100644 index 0000000000..2a9daa5d02 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtWindow.java @@ -0,0 +1,218 @@ +// 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 + +package org.qtproject.qt.android; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; + +class QtWindow extends QtLayout implements QtSurfaceInterface { + private View m_surfaceContainer; + private View m_nativeView; + private final HashMap<Integer, QtWindow> m_childWindows = new HashMap<>(); + private QtWindow m_parentWindow; + private GestureDetector m_gestureDetector; + private final QtEditText m_editText; + + private static native void setSurface(int windowId, Surface surface); + static native void windowFocusChanged(boolean hasFocus, int id); + + public QtWindow(Context context, QtWindow parentWindow, + QtInputConnection.QtInputConnectionListener listener) + { + super(context); + setId(View.generateViewId()); + m_editText = new QtEditText(context, listener); + setParent(parentWindow); + setFocusableInTouchMode(true); + addView(m_editText, new QtLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + QtNative.runAction(() -> { + m_gestureDetector = + new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent event) { + QtInputDelegate.longPress(getId(), (int) event.getX(), (int) event.getY()); + } + }); + m_gestureDetector.setIsLongpressEnabled(true); + }); + } + + @UsedFromNativeCode + void setVisible(boolean visible) { + QtNative.runAction(() -> { + if (visible) + setVisibility(View.VISIBLE); + else + setVisibility(View.INVISIBLE); + }); + } + + @Override + public void onSurfaceChanged(Surface surface) + { + setSurface(getId(), surface); + } + + @Override + public boolean onTouchEvent(MotionEvent event) + { + m_editText.requestFocus(); + event.setLocation(event.getX() + getX(), event.getY() + getY()); + QtInputDelegate.sendTouchEvent(event, getId()); + m_gestureDetector.onTouchEvent(event); + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent event) + { + QtInputDelegate.sendTrackballEvent(event, getId()); + return true; + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) + { + return QtInputDelegate.sendGenericMotionEvent(event, getId()); + } + + @UsedFromNativeCode + public void removeWindow() + { + if (m_parentWindow != null) + m_parentWindow.removeChildWindow(getId()); + } + + @UsedFromNativeCode + public void createSurface(final boolean onTop, + final int x, final int y, final int w, final int h, + final int imageDepth, final boolean isOpaque, + final int surfaceContainerType) // TODO constant for type + { + QtNative.runAction(()-> { + if (m_surfaceContainer != null) + removeView(m_surfaceContainer); + + setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); + if (surfaceContainerType == 0) { + m_surfaceContainer = new QtSurface(getContext(), QtWindow.this, + onTop, imageDepth); + } else { + m_surfaceContainer = new QtTextureView(getContext(), QtWindow.this, isOpaque); + } + m_surfaceContainer.setLayoutParams(new QtLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + // The surface container of this window will be added as the first of the stack. + // All other views are stacked based on the order they are created. + addView(m_surfaceContainer, 0); + }); + } + + @UsedFromNativeCode + public void destroySurface() + { + QtNative.runAction(()-> { + if (m_surfaceContainer != null) { + removeView(m_surfaceContainer); + m_surfaceContainer = null; + } + }, false); + } + + @UsedFromNativeCode + public void setGeometry(final int x, final int y, final int w, final int h) + { + QtNative.runAction(()-> { + if (getContext() instanceof QtActivityBase) + setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); + }); + } + + public void addChildWindow(QtWindow window) + { + QtNative.runAction(()-> { + m_childWindows.put(window.getId(), window); + addView(window, getChildCount()); + }); + } + + public void removeChildWindow(int id) + { + QtNative.runAction(()-> { + if (m_childWindows.containsKey(id)) + removeView(m_childWindows.remove(id)); + }); + } + + @UsedFromNativeCode + public void setNativeView(final View view, + final int x, final int y, final int w, final int h) + { + QtNative.runAction(()-> { + if (m_nativeView != null) + removeView(m_nativeView); + + m_nativeView = view; + QtWindow.this.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y)); + m_nativeView.setLayoutParams(new QtLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + addView(m_nativeView); + }); + } + + @UsedFromNativeCode + public void bringChildToFront(int id) + { + QtNative.runAction(()-> { + View view = m_childWindows.get(id); + if (view != null) { + if (getChildCount() > 0) + moveChild(view, getChildCount() - 1); + } + }); + } + + @UsedFromNativeCode + public void bringChildToBack(int id) { + QtNative.runAction(()-> { + View view = m_childWindows.get(id); + if (view != null) { + moveChild(view, 0); + } + }); + } + + @UsedFromNativeCode + public void removeNativeView() + { + QtNative.runAction(()-> { + if (m_nativeView != null) { + removeView(m_nativeView); + m_nativeView = null; + } + }); + } + + void setParent(QtWindow parentWindow) + { + if (m_parentWindow == parentWindow) + return; + + if (m_parentWindow != null) + m_parentWindow.removeChildWindow(getId()); + + m_parentWindow = parentWindow; + if (m_parentWindow != null) + m_parentWindow.addChildWindow(this); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtWindowInterface.java b/src/android/jar/src/org/qtproject/qt/android/QtWindowInterface.java new file mode 100644 index 0000000000..1fb312786f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtWindowInterface.java @@ -0,0 +1,11 @@ +// 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; +@UsedFromNativeCode +interface QtWindowInterface { + default void addTopLevelWindow(final QtWindow window) { } + default void removeTopLevelWindow(final int id) { } + default void bringChildToFront(final int id) { } + default void bringChildToBack(int id) { } + default void setSystemUiVisibility(int systemUiVisibility) { } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/UsedFromNativeCode.java b/src/android/jar/src/org/qtproject/qt/android/UsedFromNativeCode.java new file mode 100644 index 0000000000..e0223c083f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/UsedFromNativeCode.java @@ -0,0 +1,10 @@ +// 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 + +package org.qtproject.qt.android; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.CLASS) +public @interface UsedFromNativeCode { } diff --git a/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidBinder.java b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidBinder.java index 1270464576..bd837570fe 100644 --- a/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidBinder.java +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidBinder.java @@ -6,8 +6,11 @@ package org.qtproject.qt.android.extras; import android.os.Binder; import android.os.Parcel; -public class QtAndroidBinder extends Binder +import org.qtproject.qt.android.UsedFromNativeCode; + +class QtAndroidBinder extends Binder { + @UsedFromNativeCode public QtAndroidBinder(long id) { m_id = id; diff --git a/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidServiceConnection.java b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidServiceConnection.java index 3608051208..b70b64e3ac 100644 --- a/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidServiceConnection.java +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidServiceConnection.java @@ -7,8 +7,11 @@ import android.content.ComponentName; import android.content.ServiceConnection; import android.os.IBinder; -public class QtAndroidServiceConnection implements ServiceConnection +import org.qtproject.qt.android.UsedFromNativeCode; + +class QtAndroidServiceConnection implements ServiceConnection { + @UsedFromNativeCode public QtAndroidServiceConnection(long id) { m_id = id; diff --git a/src/android/jar/src/org/qtproject/qt/android/extras/QtNative.java b/src/android/jar/src/org/qtproject/qt/android/extras/QtNative.java index 3f847b0bb3..f7ba8dd9b4 100644 --- a/src/android/jar/src/org/qtproject/qt/android/extras/QtNative.java +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtNative.java @@ -3,12 +3,10 @@ package org.qtproject.qt.android.extras; -import android.content.ComponentName; -import android.content.ServiceConnection; import android.os.IBinder; import android.os.Parcel; -public class QtNative { +class QtNative { // Binder public static native boolean onTransact(long id, int code, Parcel data, Parcel reply, int flags); |