diff options
Diffstat (limited to 'src/android/jar/src/org/qtproject/qt/android')
38 files changed, 8631 insertions, 0 deletions
diff --git a/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java b/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java new file mode 100644 index 0000000000..7e601c0551 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/CursorHandle.java @@ -0,0 +1,199 @@ +// 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.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +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 final CursorHandle mHandle; + // The coordinate which where clicked + private float m_offsetX; + private float m_offsetY; + private boolean m_pressed = false; + + CursorView (Context context, CursorHandle handle) { + super(context); + mHandle = handle; + } + + // Called when the handle was moved programmatically , with the delta amount in pixels + public void adjusted(int dx, int dy) { + m_offsetX += dx; + m_offsetY += dy; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + m_offsetX = ev.getRawX(); + m_offsetY = ev.getRawY() + (float) getHeight() / 2; + m_pressed = true; + break; + } + + case MotionEvent.ACTION_MOVE: { + if (!m_pressed) + return false; + mHandle.updatePosition(Math.round(ev.getRawX() - m_offsetX), + Math.round(ev.getRawY() - m_offsetY)); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + m_pressed = false; + break; + } + return true; + } +} + +// Helper class that manages a cursor or selection handle +class CursorHandle implements ViewTreeObserver.OnPreDrawListener +{ + private static final String QtTag = "QtCursorHandle"; + private final View m_layout; + private CursorView m_cursorView = null; + private PopupWindow m_popup = null; + 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 final boolean m_rtl; + int m_yShift; + + public CursorHandle(Activity activity, View layout, int id, int attr, boolean rtl) { + m_activity = activity; + m_id = id; + m_attr = attr; + m_layout = layout; + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + m_yShift = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, metrics); + tolerance = Math.min(1, (int)(m_yShift / 2f)); + m_lastX = m_lastY = -1 - tolerance; + m_rtl = rtl; + } + + 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); + + 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); + if (drawable != null) { + m_popup.setWidth(drawable.getIntrinsicWidth()); + m_popup.setHeight(drawable.getIntrinsicHeight()); + } else { + Log.w(QtTag, "initOverlay(): cannot get width/height for popup " + + "from null drawable for attribute " + m_attr); + } + + m_layout.getViewTreeObserver().addOnPreDrawListener(this); + } + + // Show the handle at a given position (or move it if it is already shown) + public void setPosition(final int x, final int y){ + initOverlay(); + + final int[] layoutLocation = new int[2]; + m_layout.getLocationOnScreen(layoutLocation); + + // These values are used for handling split screen case + final int[] activityLocation = new int[2]; + final int[] activityLocationInWindow = new int[2]; + m_activity.getWindow().getDecorView().getLocationOnScreen(activityLocation); + m_activity.getWindow().getDecorView().getLocationInWindow(activityLocationInWindow); + + int x2 = x + layoutLocation[0] - activityLocation[0]; + int y2 = y + layoutLocation[1] + m_yShift + (activityLocationInWindow[1] - activityLocation[1]); + + if (m_id == QtInputDelegate.IdCursorHandle) { + x2 -= m_popup.getWidth() / 2 ; + } 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; + } + + if (m_popup.isShowing()) { + m_popup.update(x2, y2, -1, -1); + m_cursorView.adjusted(x - m_posX, y - m_posY); + } else { + m_popup.showAtLocation(m_layout, 0, x2, y2); + } + + m_posX = x; + m_posY = y; + } + + public int bottom() + { + initOverlay(); + final int[] location = new int[2]; + m_cursorView.getLocationOnScreen(location); + return location[1] + m_cursorView.getHeight(); + } + + public void hide() { + if (m_popup != null) { + m_popup.dismiss(); + } + } + + public int width() + { + return m_cursorView.getDrawable().getIntrinsicWidth(); + } + + // The handle was dragged by a given relative position + public void updatePosition(int x, int y) { + y -= m_yShift; + if (Math.abs(m_lastX - x) > tolerance || Math.abs(m_lastY - y) > tolerance) { + QtInputDelegate.handleLocationChanged(m_id, x + m_posX, y + m_posY); + m_lastX = x; + m_lastY = y; + } + } + + @Override + public boolean onPreDraw() { + // This hook is called when the view location is changed + // For example if the keyboard appears. + // Adjust the position of the handle accordingly + if (m_popup != null && m_popup.isShowing()) + setPosition(m_posX, m_posY); + + return true; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/EditContextView.java b/src/android/jar/src/org/qtproject/qt/android/EditContextView.java new file mode 100644 index 0000000000..fbd32ed98b --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/EditContextView.java @@ -0,0 +1,119 @@ +// Copyright (C) 2018 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.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 java.util.HashMap; + +@SuppressLint("ViewConstructor") +class EditContextView extends LinearLayout implements View.OnClickListener +{ + 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 SELECT_ALL_BUTTON = 1 << 3; + + HashMap<Integer, ContextButton> m_buttons = new HashMap<>(4); + OnClickListener m_onClickListener; + + public interface OnClickListener + { + void contextButtonClicked(int buttonId); + } + + private class ContextButton extends TextView + { + public int m_buttonId; + public ContextButton(Context context, int stringId) { + super(context); + m_buttonId = stringId; + setText(stringId); + setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + setGravity(Gravity.CENTER); + 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); + setPadding(hPadding, vPadding, hPadding, vPadding); + setSingleLine(); + setEllipsize(TextUtils.TruncateAt.END); + setOnClickListener(EditContextView.this); + } + } + + @Override + public void onClick(View v) + { + ContextButton button = (ContextButton)v; + m_onClickListener.contextButtonClicked(button.m_buttonId); + } + + void addButton(int id) + { + ContextButton button = new ContextButton(getContext(), id); + m_buttons.put(id, button); + addView(button); + } + + public void updateButtons(int buttonsLayout) + { + 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) { + super(context); + m_onClickListener = onClickListener; + setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + 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 new file mode 100644 index 0000000000..25be522c48 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/EditPopupMenu.java @@ -0,0 +1,153 @@ +// Copyright (C) 2018 BogDan Vatra <bogdan@kde.org> +// 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.graphics.Point; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.PopupWindow; + +// Helper class that manages a cursor or selection handle +class EditPopupMenu implements ViewTreeObserver.OnPreDrawListener, View.OnLayoutChangeListener, + EditContextView.OnClickListener +{ + private final View m_layout; + private final EditContextView m_view; + private PopupWindow m_popup = null; + private final Activity m_activity; + private int m_posX; + private int m_posY; + private int m_buttons; + private CursorHandle m_cursorHandle; + private CursorHandle m_leftSelectionHandle; + private CursorHandle m_rightSelectionHandle; + + public EditPopupMenu(Activity activity, View layout) + { + m_activity = activity; + m_view = new EditContextView(activity, this); + m_view.addOnLayoutChangeListener(this); + + m_layout = layout; + } + + private void initOverlay() + { + if (m_popup != null) + return; + + Context context = m_layout.getContext(); + m_popup = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); + m_popup.setSplitTouchEnabled(true); + m_popup.setClippingEnabled(false); + m_popup.setContentView(m_view); + m_popup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + m_popup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + + m_layout.getViewTreeObserver().addOnPreDrawListener(this); + } + + // Show the handle at a given position (or move it if it is already shown) + public void setPosition(final int x, final int y, final int buttons, + CursorHandle cursorHandle, CursorHandle leftSelectionHandle, CursorHandle rightSelectionHandle) + { + initOverlay(); + + m_view.updateButtons(buttons); + Point viewSize = m_view.getCalculatedSize(); + + final int[] layoutLocation = new int[2]; + m_layout.getLocationOnScreen(layoutLocation); + + // These values are used for handling split screen case + final int[] activityLocation = new int[2]; + final int[] activityLocationInWindow = new int[2]; + m_activity.getWindow().getDecorView().getLocationOnScreen(activityLocation); + m_activity.getWindow().getDecorView().getLocationInWindow(activityLocationInWindow); + + int x2 = x + layoutLocation[0] - activityLocation[0]; + int y2 = y + layoutLocation[1] + (activityLocationInWindow[1] - activityLocation[1]); + + x2 -= viewSize.x / 2 ; + + y2 -= viewSize.y; + if (y2 < 0) { + if (cursorHandle != null) { + y2 = cursorHandle.bottom(); + } else if (leftSelectionHandle != null && rightSelectionHandle != null) { + y2 = Math.max(leftSelectionHandle.bottom(), rightSelectionHandle.bottom()); + if (y2 <= 0) + m_layout.requestLayout(); + } + } + + if (m_layout.getWidth() < x + viewSize.x / 2) + x2 = m_layout.getWidth() - viewSize.x; + + if (x2 < 0) + x2 = 0; + + if (m_popup.isShowing()) + m_popup.update(x2, y2, -1, -1); + else + m_popup.showAtLocation(m_layout, 0, x2, y2); + + m_posX = x; + m_posY = y; + m_buttons = buttons; + m_cursorHandle = cursorHandle; + m_leftSelectionHandle = leftSelectionHandle; + m_rightSelectionHandle = rightSelectionHandle; + } + + public void hide() { + if (m_popup != null) { + m_popup.dismiss(); + m_popup = null; + } + } + + @Override + public boolean onPreDraw() { + // This hook is called when the view location is changed + // For example if the keyboard appears. + // Adjust the position of the handle accordingly + if (m_popup != null && m_popup.isShowing()) + setPosition(m_posX, m_posY, m_buttons, m_cursorHandle, m_leftSelectionHandle, m_rightSelectionHandle); + + return true; + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) + { + if ((right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) && + m_popup != null && m_popup.isShowing()) + setPosition(m_posX, m_posY, m_buttons, m_cursorHandle, m_leftSelectionHandle, m_rightSelectionHandle); + } + + @Override + public void contextButtonClicked(int buttonId) { + switch (buttonId) { + case android.R.string.cut: + QtNativeInputConnection.cut(); + break; + case android.R.string.copy: + QtNativeInputConnection.copy(); + break; + case android.R.string.paste: + QtNativeInputConnection.paste(); + break; + case android.R.string.selectAll: + QtNativeInputConnection.selectAll(); + break; + } + hide(); + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java new file mode 100644 index 0000000000..6780634317 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java @@ -0,0 +1,1858 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2014 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.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; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.NinePatch; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedStateListDrawable; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ClipDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.NinePatchDrawable; +import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.RotateDrawable; +import android.graphics.drawable.ScaleDrawable; +import android.graphics.drawable.StateListDrawable; +import android.graphics.drawable.VectorDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.view.ContextThemeWrapper; +import android.view.inputmethod.EditorInfo; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + + +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. + final int[] viewDrawableStatesState = new int[]{ + android.R.attr.state_focused, + android.R.attr.state_window_focused, + android.R.attr.state_enabled, + android.R.attr.state_selected, + android.R.attr.state_pressed, + android.R.attr.state_activated, + android.R.attr.state_accelerated, + android.R.attr.state_hovered, + android.R.attr.state_drag_can_accept, + android.R.attr.state_drag_hovered + }; + final int[] EMPTY_STATE_SET = {}; + final int[] ENABLED_STATE_SET = {android.R.attr.state_enabled}; + final int[] FOCUSED_STATE_SET = {android.R.attr.state_focused}; + final int[] SELECTED_STATE_SET = {android.R.attr.state_selected}; + final int[] PRESSED_STATE_SET = {android.R.attr.state_pressed}; + final int[] WINDOW_FOCUSED_STATE_SET = {android.R.attr.state_window_focused}; + final int[] ENABLED_FOCUSED_STATE_SET = stateSetUnion(ENABLED_STATE_SET, FOCUSED_STATE_SET); + final int[] ENABLED_SELECTED_STATE_SET = stateSetUnion(ENABLED_STATE_SET, SELECTED_STATE_SET); + final int[] ENABLED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(ENABLED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] FOCUSED_SELECTED_STATE_SET = stateSetUnion(FOCUSED_STATE_SET, SELECTED_STATE_SET); + final int[] FOCUSED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] ENABLED_FOCUSED_SELECTED_STATE_SET = stateSetUnion(ENABLED_FOCUSED_STATE_SET, SELECTED_STATE_SET); + final int[] ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(ENABLED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(ENABLED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(ENABLED_FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_SELECTED_STATE_SET = stateSetUnion(PRESSED_STATE_SET, SELECTED_STATE_SET); + final int[] PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_FOCUSED_STATE_SET = stateSetUnion(PRESSED_STATE_SET, FOCUSED_STATE_SET); + final int[] PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_FOCUSED_SELECTED_STATE_SET = stateSetUnion(PRESSED_FOCUSED_STATE_SET, SELECTED_STATE_SET); + final int[] PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_ENABLED_STATE_SET = stateSetUnion(PRESSED_STATE_SET, ENABLED_STATE_SET); + final int[] PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_ENABLED_SELECTED_STATE_SET = stateSetUnion(PRESSED_ENABLED_STATE_SET, SELECTED_STATE_SET); + final int[] PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_ENABLED_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_STATE_SET, FOCUSED_STATE_SET); + final int[] PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final int[] PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET = stateSetUnion(PRESSED_ENABLED_FOCUSED_STATE_SET, SELECTED_STATE_SET); + final int[] PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET); + final Resources.Theme m_theme; + final String m_extractPath; + final int defaultBackgroundColor; + final int defaultTextColor; + final boolean m_minimal; + final int[] DrawableStates = { android.R.attr.state_active, android.R.attr.state_checked, + android.R.attr.state_enabled, android.R.attr.state_focused, + android.R.attr.state_pressed, android.R.attr.state_selected, + android.R.attr.state_window_focused, 16908288, android.R.attr.state_multiline, + android.R.attr.state_activated, android.R.attr.state_accelerated}; + final String[] DrawableStatesLabels = {"active", "checked", "enabled", "focused", "pressed", + "selected", "window_focused", "background", "multiline", "activated", "accelerated"}; + final String[] DisableDrawableStatesLabels = {"inactive", "unchecked", "disabled", + "not_focused", "no_pressed", "unselected", "window_not_focused", "background", + "multiline", "activated", "accelerated"}; + final String[] sScaleTypeArray = { + "MATRIX", + "FIT_XY", + "FIT_START", + "FIT_CENTER", + "FIT_END", + "CENTER", + "CENTER_CROP", + "CENTER_INSIDE" + }; + Context m_context; + private final HashMap<String, DrawableCache> m_drawableCache = new HashMap<>(); + + private static boolean m_missingNormalStyle = false; + private static boolean m_missingDarkStyle = false; + private static String m_stylePath = null; + private static boolean m_extractMinimal = false; + + 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; + + if (extractOption.isEmpty()) + extractOption = "minimal"; + + 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"; + } + + // 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) { + if (m_stylePath == null) + return; + if (extractDarkMode) { + if (m_missingDarkStyle) { + new ExtractStyle(context, m_stylePath + "darkUiMode/", m_extractMinimal); + m_missingDarkStyle = false; + } + } else if (m_missingNormalStyle) { + new ExtractStyle(context, m_stylePath, m_extractMinimal); + m_missingNormalStyle = false; + } + } + + public ExtractStyle(Context context, String extractPath, boolean minimal) { + m_minimal = minimal; + m_extractPath = extractPath + "/"; + boolean dirCreated = new File(m_extractPath).mkdirs(); + if (!dirCreated) + Log.w(QtNative.QtTAG, "Cannot create Android style directory."); + m_context = context; + m_theme = context.getTheme(); + TypedArray array = m_theme.obtainStyledAttributes(new int[]{ + android.R.attr.colorBackground, + android.R.attr.textColorPrimary, + android.R.attr.textColor + }); + defaultBackgroundColor = array.getColor(0, 0); + int textColor = array.getColor(1, 0xFFFFFF); + if (textColor == 0xFFFFFF) + textColor = array.getColor(2, 0xFFFFFF); + defaultTextColor = textColor; + array.recycle(); + + try { + SimpleJsonWriter jsonWriter = new SimpleJsonWriter(m_extractPath + "style.json"); + jsonWriter.beginObject(); + try { + jsonWriter.name("defaultStyle").value(extractDefaultPalette()); + extractWindow(jsonWriter); + jsonWriter.name("buttonStyle").value(extractTextAppearanceInformation(android.R.attr.buttonStyle, "QPushButton")); + jsonWriter.name("spinnerStyle").value(extractTextAppearanceInformation(android.R.attr.spinnerStyle, "QComboBox")); + extractProgressBar(jsonWriter, android.R.attr.progressBarStyleHorizontal, "progressBarStyleHorizontal", "QProgressBar"); + extractProgressBar(jsonWriter, android.R.attr.progressBarStyleLarge, "progressBarStyleLarge", null); + extractProgressBar(jsonWriter, android.R.attr.progressBarStyleSmall, "progressBarStyleSmall", null); + extractProgressBar(jsonWriter, android.R.attr.progressBarStyle, "progressBarStyle", null); + extractAbsSeekBar(jsonWriter); + extractSwitch(jsonWriter); + extractCompoundButton(jsonWriter, android.R.attr.checkboxStyle, "checkboxStyle", "QCheckBox"); + jsonWriter.name("editTextStyle").value(extractTextAppearanceInformation(android.R.attr.editTextStyle, "QLineEdit")); + extractCompoundButton(jsonWriter, android.R.attr.radioButtonStyle, "radioButtonStyle", "QRadioButton"); + jsonWriter.name("textViewStyle").value(extractTextAppearanceInformation(android.R.attr.textViewStyle, "QWidget")); + jsonWriter.name("scrollViewStyle").value(extractTextAppearanceInformation(android.R.attr.scrollViewStyle, "QAbstractScrollArea")); + extractListView(jsonWriter); + jsonWriter.name("listSeparatorTextViewStyle").value(extractTextAppearanceInformation(android.R.attr.listSeparatorTextViewStyle, null)); + extractItemsStyle(jsonWriter); + extractCompoundButton(jsonWriter, android.R.attr.buttonStyleToggle, "buttonStyleToggle", null); + extractCalendar(jsonWriter); + extractToolBar(jsonWriter); + jsonWriter.name("actionButtonStyle").value(extractTextAppearanceInformation(android.R.attr.actionButtonStyle, "QToolButton")); + jsonWriter.name("actionBarTabTextStyle").value(extractTextAppearanceInformation(android.R.attr.actionBarTabTextStyle, null)); + jsonWriter.name("actionBarTabStyle").value(extractTextAppearanceInformation(android.R.attr.actionBarTabStyle, null)); + jsonWriter.name("actionOverflowButtonStyle").value(extractImageViewInformation(android.R.attr.actionOverflowButtonStyle, null)); + extractTabBar(jsonWriter); + } catch (Exception e) { + e.printStackTrace(); + } + jsonWriter.endObject(); + jsonWriter.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + native static int[] extractNativeChunkInfo20(long nativeChunk); + + private int[] stateSetUnion(final int[] stateSet1, final int[] stateSet2) { + try { + final int stateSet1Length = stateSet1.length; + final int stateSet2Length = stateSet2.length; + final int[] newSet = new int[stateSet1Length + stateSet2Length]; + int k = 0; + int i = 0; + int j = 0; + // This is a merge of the two input state sets and assumes that the + // input sets are sorted by the order imposed by ViewDrawableStates. + for (int viewState : viewDrawableStatesState) { + if (i < stateSet1Length && stateSet1[i] == viewState) { + newSet[k++] = viewState; + i++; + } else if (j < stateSet2Length && stateSet2[j] == viewState) { + newSet[k++] = viewState; + j++; + } + assert k <= 1 || (newSet[k - 1] > newSet[k - 2]); + } + return newSet; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + Field getAccessibleField(Class<?> clazz, String fieldName) { + try { + Field f = clazz.getDeclaredField(fieldName); + f.setAccessible(true); + return f; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + Field tryGetAccessibleField(Class<?> clazz, String fieldName) { + if (clazz == null) + return null; + + try { + Field f = clazz.getDeclaredField(fieldName); + f.setAccessible(true); + return f; + } catch (Exception e) { + for (Class<?> c : clazz.getInterfaces()) { + Field f = tryGetAccessibleField(c, fieldName); + if (f != null) + return f; + } + } + return tryGetAccessibleField(clazz.getSuperclass(), fieldName); + } + + JSONObject getColorStateList(ColorStateList colorList) { + JSONObject json = new JSONObject(); + try { + json.put("EMPTY_STATE_SET", colorList.getColorForState(EMPTY_STATE_SET, 0)); + json.put("WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(WINDOW_FOCUSED_STATE_SET, 0)); + json.put("SELECTED_STATE_SET", colorList.getColorForState(SELECTED_STATE_SET, 0)); + json.put("SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("FOCUSED_STATE_SET", colorList.getColorForState(FOCUSED_STATE_SET, 0)); + json.put("FOCUSED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(FOCUSED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("FOCUSED_SELECTED_STATE_SET", colorList.getColorForState(FOCUSED_SELECTED_STATE_SET, 0)); + json.put("FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("ENABLED_STATE_SET", colorList.getColorForState(ENABLED_STATE_SET, 0)); + json.put("ENABLED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(ENABLED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("ENABLED_SELECTED_STATE_SET", colorList.getColorForState(ENABLED_SELECTED_STATE_SET, 0)); + json.put("ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("ENABLED_FOCUSED_STATE_SET", colorList.getColorForState(ENABLED_FOCUSED_STATE_SET, 0)); + json.put("ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("ENABLED_FOCUSED_SELECTED_STATE_SET", colorList.getColorForState(ENABLED_FOCUSED_SELECTED_STATE_SET, 0)); + json.put("ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_STATE_SET", colorList.getColorForState(PRESSED_STATE_SET, 0)); + json.put("PRESSED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_SELECTED_STATE_SET", colorList.getColorForState(PRESSED_SELECTED_STATE_SET, 0)); + json.put("PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_FOCUSED_SELECTED_STATE_SET", colorList.getColorForState(PRESSED_FOCUSED_SELECTED_STATE_SET, 0)); + json.put("PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_SELECTED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_SELECTED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET, 0)); + json.put("PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET", colorList.getColorForState(PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, 0)); + } catch (JSONException e) { + e.printStackTrace(); + } + + return json; + } + + JSONObject getStatesList(int[] states) throws JSONException { + JSONObject json = new JSONObject(); + for (int s : states) { + boolean found = false; + for (int d = 0; d < DrawableStates.length; d++) { + if (s == DrawableStates[d]) { + json.put(DrawableStatesLabels[d], true); + found = true; + break; + } else if (s == -DrawableStates[d]) { + json.put(DrawableStatesLabels[d], false); + + found = true; + break; + } + } + if (!found) { + json.put("unhandled_state_" + s, s > 0); + } + } + return json; + } + + String getStatesName(int[] states) { + StringBuilder statesName = new StringBuilder(); + for (int s : states) { + boolean found = false; + for (int d = 0; d < DrawableStates.length; d++) { + if (s == DrawableStates[d]) { + if (statesName.length() > 0) + statesName.append("__"); + statesName.append(DrawableStatesLabels[d]); + found = true; + break; + } else if (s == -DrawableStates[d]) { + if (statesName.length() > 0) + statesName.append("__"); + statesName.append(DisableDrawableStatesLabels[d]); + found = true; + break; + } + } + if (!found) { + if (statesName.length() > 0) + statesName.append(";"); + statesName.append(s); + } + } + if (statesName.length() > 0) + return statesName.toString(); + return "empty"; + } + + private JSONObject getLayerDrawable(Object drawable, String filename) { + JSONObject json = new JSONObject(); + LayerDrawable layers = (LayerDrawable) drawable; + final int nr = layers.getNumberOfLayers(); + try { + JSONArray array = new JSONArray(); + for (int i = 0; i < nr; i++) { + int id = layers.getId(i); + if (id == -1) + id = i; + JSONObject layerJsonObject = getDrawable(layers.getDrawable(i), filename + "__" + id, null); + layerJsonObject.put("id", id); + array.put(layerJsonObject); + } + json.put("type", "layer"); + Rect padding = new Rect(); + if (layers.getPadding(padding)) + json.put("padding", getJsonRect(padding)); + json.put("layers", array); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getStateListDrawable(Object drawable, String filename) { + JSONObject json = new JSONObject(); + try { + StateListDrawable stateList = (StateListDrawable) drawable; + JSONArray array = new JSONArray(); + final int numStates; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + numStates = (Integer) StateListDrawable.class.getMethod("getStateCount").invoke(stateList); + else + numStates = stateList.getStateCount(); + for (int i = 0; i < numStates; i++) { + JSONObject stateJson = new JSONObject(); + final Drawable d = (Drawable) StateListDrawable.class.getMethod("getStateDrawable", Integer.TYPE).invoke(stateList, i); + final int[] states = (int[]) StateListDrawable.class.getMethod("getStateSet", Integer.TYPE).invoke(stateList, i); + if (states != null) + stateJson.put("states", getStatesList(states)); + stateJson.put("drawable", getDrawable(d, filename + "__" + (states != null ? getStatesName(states) : ("state_pos_" + i)), null)); + array.put(stateJson); + } + json.put("type", "stateslist"); + Rect padding = new Rect(); + if (stateList.getPadding(padding)) + json.put("padding", getJsonRect(padding)); + json.put("stateslist", array); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getGradientDrawable(GradientDrawable drawable) { + JSONObject json = new JSONObject(); + try { + json.put("type", "gradient"); + Object obj = drawable.getConstantState(); + Class<?> gradientStateClass = obj.getClass(); + json.put("shape", gradientStateClass.getField("mShape").getInt(obj)); + json.put("gradient", gradientStateClass.getField("mGradient").getInt(obj)); + GradientDrawable.Orientation orientation = (Orientation) gradientStateClass.getField("mOrientation").get(obj); + if (orientation != null) + json.put("orientation", orientation.name()); + int[] intArray = (int[]) gradientStateClass.getField("mGradientColors").get(obj); + if (intArray != null) + json.put("colors", getJsonArray(intArray, 0, intArray.length)); + json.put("positions", getJsonArray((float[]) gradientStateClass.getField("mPositions").get(obj))); + json.put("strokeWidth", gradientStateClass.getField("mStrokeWidth").getInt(obj)); + json.put("strokeDashWidth", gradientStateClass.getField("mStrokeDashWidth").getFloat(obj)); + json.put("strokeDashGap", gradientStateClass.getField("mStrokeDashGap").getFloat(obj)); + json.put("radius", gradientStateClass.getField("mRadius").getFloat(obj)); + float[] floatArray = (float[]) gradientStateClass.getField("mRadiusArray").get(obj); + if (floatArray != null) + json.put("radiusArray", getJsonArray(floatArray)); + Rect rc = (Rect) gradientStateClass.getField("mPadding").get(obj); + if (rc != null) + json.put("padding", getJsonRect(rc)); + json.put("width", gradientStateClass.getField("mWidth").getInt(obj)); + json.put("height", gradientStateClass.getField("mHeight").getInt(obj)); + json.put("innerRadiusRatio", gradientStateClass.getField("mInnerRadiusRatio").getFloat(obj)); + json.put("thicknessRatio", gradientStateClass.getField("mThicknessRatio").getFloat(obj)); + json.put("innerRadius", gradientStateClass.getField("mInnerRadius").getInt(obj)); + json.put("thickness", gradientStateClass.getField("mThickness").getInt(obj)); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getRotateDrawable(RotateDrawable drawable, String filename) { + JSONObject json = new JSONObject(); + try { + json.put("type", "rotate"); + Object obj = drawable.getConstantState(); + Class<?> rotateStateClass = obj.getClass(); + json.put("drawable", getDrawable(drawable.getClass().getMethod("getDrawable").invoke(drawable), filename, null)); + json.put("pivotX", getAccessibleField(rotateStateClass, "mPivotX").getFloat(obj)); + json.put("pivotXRel", getAccessibleField(rotateStateClass, "mPivotXRel").getBoolean(obj)); + json.put("pivotY", getAccessibleField(rotateStateClass, "mPivotY").getFloat(obj)); + json.put("pivotYRel", getAccessibleField(rotateStateClass, "mPivotYRel").getBoolean(obj)); + json.put("fromDegrees", getAccessibleField(rotateStateClass, "mFromDegrees").getFloat(obj)); + json.put("toDegrees", getAccessibleField(rotateStateClass, "mToDegrees").getFloat(obj)); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getAnimationDrawable(AnimationDrawable drawable, String filename) { + JSONObject json = new JSONObject(); + try { + json.put("type", "animation"); + json.put("oneshot", drawable.isOneShot()); + final int count = drawable.getNumberOfFrames(); + JSONArray frames = new JSONArray(); + for (int i = 0; i < count; ++i) { + JSONObject frame = new JSONObject(); + frame.put("duration", drawable.getDuration(i)); + frame.put("drawable", getDrawable(drawable.getFrame(i), filename + "__" + i, null)); + frames.put(frame); + } + json.put("frames", frames); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getJsonRect(Rect rect) throws JSONException { + JSONObject jsonRect = new JSONObject(); + jsonRect.put("left", rect.left); + jsonRect.put("top", rect.top); + jsonRect.put("right", rect.right); + jsonRect.put("bottom", rect.bottom); + return jsonRect; + + } + + private JSONArray getJsonArray(int[] array, int pos, int len) { + JSONArray a = new JSONArray(); + final int l = pos + len; + for (int i = pos; i < l; i++) + a.put(array[i]); + return a; + } + + private JSONArray getJsonArray(float[] array) throws JSONException { + JSONArray a = new JSONArray(); + if (array != null) + for (float val : array) + a.put(val); + return a; + } + + private JSONObject getJsonChunkInfo(int[] chunkData) throws JSONException { + JSONObject jsonRect = new JSONObject(); + if (chunkData == null) + return jsonRect; + + jsonRect.put("xdivs", getJsonArray(chunkData, 3, chunkData[0])); + jsonRect.put("ydivs", getJsonArray(chunkData, 3 + chunkData[0], chunkData[1])); + jsonRect.put("colors", getJsonArray(chunkData, 3 + chunkData[0] + chunkData[1], chunkData[2])); + return jsonRect; + } + + private JSONObject findPatchesMarings(Drawable d) throws JSONException, IllegalAccessException { + NinePatch np; + Field f = tryGetAccessibleField(NinePatchDrawable.class, "mNinePatch"); + if (f != null) { + np = (NinePatch) f.get(d); + } else { + Object state = getAccessibleField(NinePatchDrawable.class, "mNinePatchState").get(d); + np = (NinePatch) getAccessibleField(Objects.requireNonNull(state).getClass(), "mNinePatch").get(state); + } + return getJsonChunkInfo(extractNativeChunkInfo20(getAccessibleField(Objects.requireNonNull(np).getClass(), "mNativeChunk").getLong(np))); + } + + private JSONObject getRippleDrawable(Object drawable, String filename, Rect padding) { + JSONObject json = getLayerDrawable(drawable, filename); + JSONObject ripple = new JSONObject(); + try { + Class<?> rippleDrawableClass = Class.forName("android.graphics.drawable.RippleDrawable"); + final Object mState = getAccessibleField(rippleDrawableClass, "mState").get(drawable); + ripple.put("mask", getDrawable((Drawable) getAccessibleField(rippleDrawableClass, "mMask").get(drawable), filename, padding)); + if (mState != null) { + ripple.put("maxRadius", getAccessibleField(mState.getClass(), "mMaxRadius").getInt(mState)); + ColorStateList color = (ColorStateList) getAccessibleField(mState.getClass(), "mColor").get(mState); + if (color != null) + ripple.put("color", getColorStateList(color)); + } + json.put("ripple", ripple); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private HashMap<Long, Long> getStateTransitions(Object sa) throws Exception { + HashMap<Long, Long> transitions = new HashMap<>(); + final int sz = getAccessibleField(sa.getClass(), "mSize").getInt(sa); + long[] keys = (long[]) getAccessibleField(sa.getClass(), "mKeys").get(sa); + long[] values = (long[]) getAccessibleField(sa.getClass(), "mValues").get(sa); + for (int i = 0; i < sz; i++) { + if (keys != null && values != null) + transitions.put(keys[i], values[i]); + } + return transitions; + } + + private HashMap<Integer, Integer> getStateIds(Object sa) throws Exception { + HashMap<Integer, Integer> states = new HashMap<>(); + final int sz = getAccessibleField(sa.getClass(), "mSize").getInt(sa); + int[] keys = (int[]) getAccessibleField(sa.getClass(), "mKeys").get(sa); + int[] values = (int[]) getAccessibleField(sa.getClass(), "mValues").get(sa); + for (int i = 0; i < sz; i++) { + if (keys != null && values != null) + states.put(keys[i], values[i]); + } + return states; + } + + private int findStateIndex(int id, HashMap<Integer, Integer> stateIds) { + for (Map.Entry<Integer, Integer> s : stateIds.entrySet()) { + if (id == s.getValue()) + return s.getKey(); + } + return -1; + } + + private JSONObject getAnimatedStateListDrawable(Object drawable, String filename) { + JSONObject json = getStateListDrawable(drawable, filename); + try { + Class<?> animatedStateListDrawableClass = Class.forName("android.graphics.drawable.AnimatedStateListDrawable"); + Object state = getAccessibleField(animatedStateListDrawableClass, "mState").get(drawable); + + if (state != null) { + Class<?> stateClass = state.getClass(); + HashMap<Integer, Integer> stateIds = getStateIds(Objects.requireNonNull(getAccessibleField(stateClass, "mStateIds").get(state))); + HashMap<Long, Long> transitions = getStateTransitions(Objects.requireNonNull(getAccessibleField(stateClass, "mTransitions").get(state))); + + for (Map.Entry<Long, Long> t : transitions.entrySet()) { + final int toState = findStateIndex(t.getKey().intValue(), stateIds); + final int fromState = findStateIndex((int) (t.getKey() >> 32), stateIds); + + JSONObject transition = new JSONObject(); + transition.put("from", fromState); + transition.put("to", toState); + transition.put("reverse", (t.getValue() >> 32) != 0); + + JSONArray stateslist = json.getJSONArray("stateslist"); + JSONObject stateobj = stateslist.getJSONObject(t.getValue().intValue()); + stateobj.put("transition", transition); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject getVPath(Object path) throws Exception { + JSONObject json = new JSONObject(); + final Class<?> pathClass = path.getClass(); + json.put("type", "path"); + json.put("name", tryGetAccessibleField(pathClass, "mPathName").get(path)); + Object[] mNodes = (Object[]) tryGetAccessibleField(pathClass, "mNodes").get(path); + JSONArray nodes = new JSONArray(); + if (mNodes != null) { + for (Object n : mNodes) { + JSONObject node = new JSONObject(); + node.put("type", String.valueOf(getAccessibleField(n.getClass(), "mType").getChar(n))); + node.put("params", getJsonArray((float[]) getAccessibleField(n.getClass(), "mParams").get(n))); + nodes.put(node); + } + json.put("nodes", nodes); + } + json.put("isClip", (Boolean) pathClass.getMethod("isClipPath").invoke(path)); + + if (tryGetAccessibleField(pathClass, "mStrokeColor") == null) + return json; // not VFullPath + + json.put("strokeColor", getAccessibleField(pathClass, "mStrokeColor").getInt(path)); + json.put("strokeWidth", getAccessibleField(pathClass, "mStrokeWidth").getFloat(path)); + json.put("fillColor", getAccessibleField(pathClass, "mFillColor").getInt(path)); + json.put("strokeAlpha", getAccessibleField(pathClass, "mStrokeAlpha").getFloat(path)); + json.put("fillRule", getAccessibleField(pathClass, "mFillRule").getInt(path)); + json.put("fillAlpha", getAccessibleField(pathClass, "mFillAlpha").getFloat(path)); + json.put("trimPathStart", getAccessibleField(pathClass, "mTrimPathStart").getFloat(path)); + json.put("trimPathEnd", getAccessibleField(pathClass, "mTrimPathEnd").getFloat(path)); + json.put("trimPathOffset", getAccessibleField(pathClass, "mTrimPathOffset").getFloat(path)); + json.put("strokeLineCap", (Paint.Cap) getAccessibleField(pathClass, "mStrokeLineCap").get(path)); + json.put("strokeLineJoin", (Paint.Join) getAccessibleField(pathClass, "mStrokeLineJoin").get(path)); + json.put("strokeMiterlimit", getAccessibleField(pathClass, "mStrokeMiterlimit").getFloat(path)); + return json; + } + + @SuppressWarnings("unchecked") + private JSONObject getVGroup(Object group) throws Exception { + JSONObject json = new JSONObject(); + json.put("type", "group"); + final Class<?> groupClass = group.getClass(); + json.put("name", getAccessibleField(groupClass, "mGroupName").get(group)); + json.put("rotate", getAccessibleField(groupClass, "mRotate").getFloat(group)); + json.put("pivotX", getAccessibleField(groupClass, "mPivotX").getFloat(group)); + json.put("pivotY", getAccessibleField(groupClass, "mPivotY").getFloat(group)); + json.put("scaleX", getAccessibleField(groupClass, "mScaleX").getFloat(group)); + json.put("scaleY", getAccessibleField(groupClass, "mScaleY").getFloat(group)); + json.put("translateX", getAccessibleField(groupClass, "mTranslateX").getFloat(group)); + json.put("translateY", getAccessibleField(groupClass, "mTranslateY").getFloat(group)); + + ArrayList<Object> mChildren = (ArrayList<Object>) getAccessibleField(groupClass, "mChildren").get(group); + JSONArray children = new JSONArray(); + if (mChildren != null) { + for (Object c : mChildren) { + if (groupClass.isInstance(c)) + children.put(getVGroup(c)); + else + children.put(getVPath(c)); + } + json.put("children", children); + } + return json; + } + + private JSONObject getVectorDrawable(Object drawable) { + JSONObject json = new JSONObject(); + try { + json.put("type", "vector"); + Class<?> vectorDrawableClass = Class.forName("android.graphics.drawable.VectorDrawable"); + final Object state = getAccessibleField(vectorDrawableClass, "mVectorState").get(drawable); + final Class<?> stateClass = Objects.requireNonNull(state).getClass(); + final ColorStateList mTint = (ColorStateList) getAccessibleField(stateClass, "mTint").get(state); + if (mTint != null) { + json.put("tintList", getColorStateList(mTint)); + json.put("tintMode", (PorterDuff.Mode) getAccessibleField(stateClass, "mTintMode").get(state)); + } + final Object mVPathRenderer = getAccessibleField(stateClass, "mVPathRenderer").get(state); + final Class<?> VPathRendererClass = Objects.requireNonNull(mVPathRenderer).getClass(); + json.put("baseWidth", getAccessibleField(VPathRendererClass, "mBaseWidth").getFloat(mVPathRenderer)); + json.put("baseHeight", getAccessibleField(VPathRendererClass, "mBaseHeight").getFloat(mVPathRenderer)); + json.put("viewportWidth", getAccessibleField(VPathRendererClass, "mViewportWidth").getFloat(mVPathRenderer)); + json.put("viewportHeight", getAccessibleField(VPathRendererClass, "mViewportHeight").getFloat(mVPathRenderer)); + json.put("rootAlpha", getAccessibleField(VPathRendererClass, "mRootAlpha").getInt(mVPathRenderer)); + json.put("rootName", getAccessibleField(VPathRendererClass, "mRootName").get(mVPathRenderer)); + json.put("rootGroup", getVGroup(Objects.requireNonNull(getAccessibleField(VPathRendererClass, "mRootGroup").get(mVPathRenderer)))); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + public JSONObject getDrawable(Object drawable, String filename, Rect padding) { + if (drawable == null || m_minimal) + return null; + + DrawableCache dc = m_drawableCache.get(filename); + if (dc != null) { + if (dc.drawable.equals(drawable)) + return dc.object; + else + Log.e(QtNative.QtTAG, "Different drawable objects points to the same file name \"" + filename + "\""); + } + JSONObject json = new JSONObject(); + Bitmap bmp = null; + if (drawable instanceof Bitmap) + bmp = (Bitmap) drawable; + else { + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + bmp = bitmapDrawable.getBitmap(); + try { + json.put("gravity", bitmapDrawable.getGravity()); + json.put("tileModeX", bitmapDrawable.getTileModeX()); + json.put("tileModeY", bitmapDrawable.getTileModeY()); + json.put("antialias", (Boolean) BitmapDrawable.class.getMethod("hasAntiAlias").invoke(bitmapDrawable)); + json.put("mipMap", (Boolean) BitmapDrawable.class.getMethod("hasMipMap").invoke(bitmapDrawable)); + json.put("tintMode", (PorterDuff.Mode) BitmapDrawable.class.getMethod("getTintMode").invoke(bitmapDrawable)); + ColorStateList tintList = (ColorStateList) BitmapDrawable.class.getMethod("getTint").invoke(bitmapDrawable); + if (tintList != null) + json.put("tintList", getColorStateList(tintList)); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + + if (drawable instanceof RippleDrawable) + return getRippleDrawable(drawable, filename, padding); + + if (drawable instanceof AnimatedStateListDrawable) + return getAnimatedStateListDrawable(drawable, filename); + + if (drawable instanceof VectorDrawable) + return getVectorDrawable(drawable); + + if (drawable instanceof ScaleDrawable) { + return getDrawable(((ScaleDrawable) drawable).getDrawable(), filename, null); + } + if (drawable instanceof LayerDrawable) { + return getLayerDrawable(drawable, filename); + } + if (drawable instanceof StateListDrawable) { + return getStateListDrawable(drawable, filename); + } + if (drawable instanceof GradientDrawable) { + return getGradientDrawable((GradientDrawable) drawable); + } + if (drawable instanceof RotateDrawable) { + return getRotateDrawable((RotateDrawable) drawable, filename); + } + if (drawable instanceof AnimationDrawable) { + return getAnimationDrawable((AnimationDrawable) drawable, filename); + } + if (drawable instanceof ClipDrawable) { + try { + json.put("type", "clipDrawable"); + Drawable.ConstantState dcs = ((ClipDrawable) drawable).getConstantState(); + json.put("drawable", getDrawable(getAccessibleField(dcs.getClass(), "mDrawable").get(dcs), filename, null)); + if (null != padding) + json.put("padding", getJsonRect(padding)); + else { + Rect _padding = new Rect(); + if (((Drawable) drawable).getPadding(_padding)) + json.put("padding", getJsonRect(_padding)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + if (drawable instanceof ColorDrawable) { + bmp = Bitmap.createBitmap(1, 1, Config.ARGB_8888); + Drawable d = (Drawable) drawable; + d.setBounds(0, 0, 1, 1); + d.draw(new Canvas(bmp)); + try { + json.put("type", "color"); + json.put("color", bmp.getPixel(0, 0)); + if (null != padding) + json.put("padding", getJsonRect(padding)); + else { + Rect _padding = new Rect(); + if (d.getPadding(_padding)) + json.put("padding", getJsonRect(_padding)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + if (drawable instanceof InsetDrawable) { + try { + InsetDrawable d = (InsetDrawable) drawable; + Object mInsetStateObject = getAccessibleField(InsetDrawable.class, "mState").get(d); + Rect _padding = new Rect(); + boolean hasPadding = d.getPadding(_padding); + return getDrawable(getAccessibleField(Objects.requireNonNull(mInsetStateObject).getClass(), "mDrawable").get(mInsetStateObject), filename, hasPadding ? _padding : null); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + Drawable d = (Drawable) drawable; + int w = d.getIntrinsicWidth(); + int h = d.getIntrinsicHeight(); + d.setLevel(10000); + if (w < 1 || h < 1) { + w = 100; + h = 100; + } + bmp = Bitmap.createBitmap(w, h, Config.ARGB_8888); + d.setBounds(0, 0, w, h); + d.draw(new Canvas(bmp)); + if (drawable instanceof NinePatchDrawable) { + NinePatchDrawable npd = (NinePatchDrawable) drawable; + try { + json.put("type", "9patch"); + json.put("drawable", getDrawable(bmp, filename, null)); + if (padding != null) + json.put("padding", getJsonRect(padding)); + else { + Rect _padding = new Rect(); + if (npd.getPadding(_padding)) + json.put("padding", getJsonRect(_padding)); + } + + json.put("chunkInfo", findPatchesMarings(d)); + return json; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + FileOutputStream out; + try { + filename = m_extractPath + filename + ".png"; + out = new FileOutputStream(filename); + if (bmp != null) + bmp.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + json.put("type", "image"); + json.put("path", filename); + if (bmp != null) { + json.put("width", bmp.getWidth()); + json.put("height", bmp.getHeight()); + } + m_drawableCache.put(filename, new DrawableCache(json, drawable)); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + + private TypedArray obtainStyledAttributes(int styleName, int[] attributes) + { + TypedValue typedValue = new TypedValue(); + Context ctx = new ContextThemeWrapper(m_context, m_theme); + ctx.getTheme().resolveAttribute(styleName, typedValue, true); + return ctx.obtainStyledAttributes(typedValue.data, attributes); + } + + private ArrayList<Integer> getArrayListFromIntArray(int[] attributes) { + ArrayList<Integer> sortedAttrs = new ArrayList<>(); + for (int attr : attributes) + sortedAttrs.add(attr); + return sortedAttrs; + } + + public void extractViewInformation(int styleName, JSONObject json, String qtClassName) { + extractViewInformation(styleName, json, qtClassName, null); + } + + public void extractViewInformation(int styleName, JSONObject json, String qtClassName, AttributeSet attributeSet) { + try { + TypedValue typedValue = new TypedValue(); + Context ctx = new ContextThemeWrapper(m_context, m_theme); + ctx.getTheme().resolveAttribute(styleName, typedValue, true); + + int[] attributes = new int[]{ + android.R.attr.digits, + android.R.attr.background, + android.R.attr.padding, + android.R.attr.paddingLeft, + android.R.attr.paddingTop, + android.R.attr.paddingRight, + android.R.attr.paddingBottom, + android.R.attr.scrollX, + android.R.attr.scrollY, + android.R.attr.id, + android.R.attr.tag, + android.R.attr.fitsSystemWindows, + android.R.attr.focusable, + android.R.attr.focusableInTouchMode, + android.R.attr.clickable, + android.R.attr.longClickable, + android.R.attr.saveEnabled, + android.R.attr.duplicateParentState, + android.R.attr.visibility, + android.R.attr.drawingCacheQuality, + android.R.attr.contentDescription, + android.R.attr.soundEffectsEnabled, + android.R.attr.hapticFeedbackEnabled, + android.R.attr.scrollbars, + android.R.attr.fadingEdge, + android.R.attr.scrollbarStyle, + android.R.attr.scrollbarFadeDuration, + android.R.attr.scrollbarDefaultDelayBeforeFade, + android.R.attr.scrollbarSize, + android.R.attr.scrollbarThumbHorizontal, + android.R.attr.scrollbarThumbVertical, + android.R.attr.scrollbarTrackHorizontal, + android.R.attr.scrollbarTrackVertical, + android.R.attr.isScrollContainer, + android.R.attr.keepScreenOn, + android.R.attr.filterTouchesWhenObscured, + android.R.attr.nextFocusLeft, + android.R.attr.nextFocusRight, + android.R.attr.nextFocusUp, + android.R.attr.nextFocusDown, + android.R.attr.minWidth, + android.R.attr.minHeight, + android.R.attr.onClick, + android.R.attr.overScrollMode, + android.R.attr.paddingStart, + android.R.attr.paddingEnd, + }; + + // The array must be sorted in ascending order, otherwise obtainStyledAttributes() + // might fail to find some attributes + Arrays.sort(attributes); + TypedArray array; + if (attributeSet != null) + array = m_theme.obtainStyledAttributes(attributeSet, attributes, styleName, 0); + else + array = obtainStyledAttributes(styleName, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + if (null != qtClassName) + json.put("qtClass", qtClassName); + + json.put("defaultBackgroundColor", defaultBackgroundColor); + json.put("defaultTextColorPrimary", defaultTextColor); + json.put("TextView_digits", array.getText(sortedAttrs.indexOf(android.R.attr.digits))); + json.put("View_background", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.background)), styleName + "_View_background", null)); + json.put("View_padding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.padding), -1)); + json.put("View_paddingLeft", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingLeft), -1)); + json.put("View_paddingTop", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingTop), -1)); + json.put("View_paddingRight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingRight), -1)); + json.put("View_paddingBottom", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingBottom), -1)); + json.put("View_paddingBottom", array.getDimensionPixelOffset(sortedAttrs.indexOf(android.R.attr.scrollX), 0)); + json.put("View_scrollY", array.getDimensionPixelOffset(sortedAttrs.indexOf(android.R.attr.scrollY), 0)); + json.put("View_id", array.getResourceId(sortedAttrs.indexOf(android.R.attr.id), -1)); + json.put("View_tag", array.getText(sortedAttrs.indexOf(android.R.attr.tag))); + json.put("View_fitsSystemWindows", array.getBoolean(sortedAttrs.indexOf(android.R.attr.fitsSystemWindows), false)); + json.put("View_focusable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.focusable), false)); + json.put("View_focusableInTouchMode", array.getBoolean(sortedAttrs.indexOf(android.R.attr.focusableInTouchMode), false)); + json.put("View_clickable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.clickable), false)); + json.put("View_longClickable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.longClickable), false)); + json.put("View_saveEnabled", array.getBoolean(sortedAttrs.indexOf(android.R.attr.saveEnabled), true)); + json.put("View_duplicateParentState", array.getBoolean(sortedAttrs.indexOf(android.R.attr.duplicateParentState), false)); + json.put("View_visibility", array.getInt(sortedAttrs.indexOf(android.R.attr.visibility), 0)); + json.put("View_drawingCacheQuality", array.getInt(sortedAttrs.indexOf(android.R.attr.drawingCacheQuality), 0)); + json.put("View_contentDescription", array.getString(sortedAttrs.indexOf(android.R.attr.contentDescription))); + json.put("View_soundEffectsEnabled", array.getBoolean(sortedAttrs.indexOf(android.R.attr.soundEffectsEnabled), true)); + json.put("View_hapticFeedbackEnabled", array.getBoolean(sortedAttrs.indexOf(android.R.attr.hapticFeedbackEnabled), true)); + json.put("View_scrollbars", array.getInt(sortedAttrs.indexOf(android.R.attr.scrollbars), 0)); + json.put("View_fadingEdge", array.getInt(sortedAttrs.indexOf(android.R.attr.fadingEdge), 0)); + json.put("View_scrollbarStyle", array.getInt(sortedAttrs.indexOf(android.R.attr.scrollbarStyle), 0)); + json.put("View_scrollbarFadeDuration", array.getInt(sortedAttrs.indexOf(android.R.attr.scrollbarFadeDuration), 0)); + json.put("View_scrollbarDefaultDelayBeforeFade", array.getInt(sortedAttrs.indexOf(android.R.attr.scrollbarDefaultDelayBeforeFade), 0)); + json.put("View_scrollbarSize", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.scrollbarSize), -1)); + json.put("View_scrollbarThumbHorizontal", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.scrollbarThumbHorizontal)), styleName + "_View_scrollbarThumbHorizontal", null)); + json.put("View_scrollbarThumbVertical", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.scrollbarThumbVertical)), styleName + "_View_scrollbarThumbVertical", null)); + json.put("View_scrollbarTrackHorizontal", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.scrollbarTrackHorizontal)), styleName + "_View_scrollbarTrackHorizontal", null)); + json.put("View_scrollbarTrackVertical", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.scrollbarTrackVertical)), styleName + "_View_scrollbarTrackVertical", null)); + json.put("View_isScrollContainer", array.getBoolean(sortedAttrs.indexOf(android.R.attr.isScrollContainer), false)); + json.put("View_keepScreenOn", array.getBoolean(sortedAttrs.indexOf(android.R.attr.keepScreenOn), false)); + json.put("View_filterTouchesWhenObscured", array.getBoolean(sortedAttrs.indexOf(android.R.attr.filterTouchesWhenObscured), false)); + json.put("View_nextFocusLeft", array.getResourceId(sortedAttrs.indexOf(android.R.attr.nextFocusLeft), -1)); + json.put("View_nextFocusRight", array.getResourceId(sortedAttrs.indexOf(android.R.attr.nextFocusRight), -1)); + json.put("View_nextFocusUp", array.getResourceId(sortedAttrs.indexOf(android.R.attr.nextFocusUp), -1)); + json.put("View_nextFocusDown", array.getResourceId(sortedAttrs.indexOf(android.R.attr.nextFocusDown), -1)); + json.put("View_minWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minWidth), 0)); + json.put("View_minHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minHeight), 0)); + json.put("View_onClick", array.getString(sortedAttrs.indexOf(android.R.attr.onClick))); + json.put("View_overScrollMode", array.getInt(sortedAttrs.indexOf(android.R.attr.overScrollMode), 1)); + json.put("View_paddingStart", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingStart), 0)); + json.put("View_paddingEnd", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.paddingEnd), 0)); + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public JSONObject extractTextAppearance(int styleName) + { + return extractTextAppearance(styleName, false); + } + + @SuppressLint("ResourceType") + public JSONObject extractTextAppearance(int styleName, boolean subStyle) + { + final int[] attributes = new int[]{ + android.R.attr.textSize, + android.R.attr.textStyle, + android.R.attr.textColor, + android.R.attr.typeface, + android.R.attr.textAllCaps, + android.R.attr.textColorHint, + android.R.attr.textColorLink, + android.R.attr.textColorHighlight + }; + Arrays.sort(attributes); + TypedArray array; + if (subStyle) + array = m_theme.obtainStyledAttributes(styleName, attributes); + else + array = obtainStyledAttributes(styleName, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + JSONObject json = new JSONObject(); + try { + int attr = sortedAttrs.indexOf(android.R.attr.textSize); + if (array.hasValue(attr)) + json.put("TextAppearance_textSize", array.getDimensionPixelSize(attr, 15)); + attr = sortedAttrs.indexOf(android.R.attr.textStyle); + if (array.hasValue(attr)) + json.put("TextAppearance_textStyle", array.getInt(attr, -1)); + ColorStateList color = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColor)); + if (color != null) + json.put("TextAppearance_textColor", getColorStateList(color)); + attr = sortedAttrs.indexOf(android.R.attr.typeface); + if (array.hasValue(attr)) + json.put("TextAppearance_typeface", array.getInt(attr, -1)); + attr = sortedAttrs.indexOf(android.R.attr.textAllCaps); + if (array.hasValue(attr)) + json.put("TextAppearance_textAllCaps", array.getBoolean(attr, false)); + color = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColorHint)); + if (color != null) + json.put("TextAppearance_textColorHint", getColorStateList(color)); + color = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColorLink)); + if (color != null) + json.put("TextAppearance_textColorLink", getColorStateList(color)); + attr = sortedAttrs.indexOf(android.R.attr.textColorHighlight); + if (array.hasValue(attr)) + json.put("TextAppearance_textColorHighlight", array.getColor(attr, 0)); + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + public JSONObject extractTextAppearanceInformation(int styleName, String qtClass) { + return extractTextAppearanceInformation(styleName, qtClass, android.R.attr.textAppearance, null); + } + + public JSONObject extractTextAppearanceInformation(int styleName, String qtClass, int textAppearance, AttributeSet attributeSet) { + JSONObject json = new JSONObject(); + extractViewInformation(styleName, json, qtClass, attributeSet); + + if (textAppearance == -1) + textAppearance = android.R.attr.textAppearance; + + try { + TypedValue typedValue = new TypedValue(); + Context ctx = new ContextThemeWrapper(m_context, m_theme); + ctx.getTheme().resolveAttribute(styleName, typedValue, true); + + // Get textAppearance values + int[] textAppearanceAttr = new int[]{textAppearance}; + TypedArray textAppearanceArray = ctx.obtainStyledAttributes(typedValue.data, textAppearanceAttr); + int textAppearanceId = textAppearanceArray.getResourceId(0, -1); + textAppearanceArray.recycle(); + + int textSize = 15; + int styleIndex = -1; + int typefaceIndex = -1; + int textColorHighlight = 0; + boolean allCaps = false; + + if (textAppearanceId != -1) { + int[] attributes = new int[]{ + android.R.attr.textSize, + android.R.attr.textStyle, + android.R.attr.typeface, + android.R.attr.textAllCaps, + android.R.attr.textColorHighlight + }; + Arrays.sort(attributes); + TypedArray array = m_theme.obtainStyledAttributes(textAppearanceId, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + textSize = array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.textSize), 15); + styleIndex = array.getInt(sortedAttrs.indexOf(android.R.attr.textStyle), -1); + typefaceIndex = array.getInt(sortedAttrs.indexOf(android.R.attr.typeface), -1); + textColorHighlight = array.getColor(sortedAttrs.indexOf(android.R.attr.textColorHighlight), 0); + allCaps = array.getBoolean(sortedAttrs.indexOf(android.R.attr.textAllCaps), false); + array.recycle(); + } + // Get TextView values + int[] attributes = new int[]{ + android.R.attr.editable, + android.R.attr.inputMethod, + android.R.attr.numeric, + android.R.attr.digits, + android.R.attr.phoneNumber, + android.R.attr.autoText, + android.R.attr.capitalize, + android.R.attr.bufferType, + android.R.attr.selectAllOnFocus, + android.R.attr.autoLink, + android.R.attr.linksClickable, + android.R.attr.drawableLeft, + android.R.attr.drawableTop, + android.R.attr.drawableRight, + android.R.attr.drawableBottom, + android.R.attr.drawableStart, + android.R.attr.drawableEnd, + android.R.attr.maxLines, + android.R.attr.drawablePadding, + android.R.attr.textCursorDrawable, + android.R.attr.maxHeight, + android.R.attr.lines, + android.R.attr.height, + android.R.attr.minLines, + android.R.attr.minHeight, + android.R.attr.maxEms, + android.R.attr.maxWidth, + android.R.attr.ems, + android.R.attr.width, + android.R.attr.minEms, + android.R.attr.minWidth, + android.R.attr.gravity, + android.R.attr.hint, + android.R.attr.text, + android.R.attr.scrollHorizontally, + android.R.attr.singleLine, + android.R.attr.ellipsize, + android.R.attr.marqueeRepeatLimit, + android.R.attr.includeFontPadding, + android.R.attr.cursorVisible, + android.R.attr.maxLength, + android.R.attr.textScaleX, + android.R.attr.freezesText, + android.R.attr.shadowColor, + android.R.attr.shadowDx, + android.R.attr.shadowDy, + android.R.attr.shadowRadius, + android.R.attr.enabled, + android.R.attr.textColorHighlight, + android.R.attr.textColor, + android.R.attr.textColorHint, + android.R.attr.textColorLink, + android.R.attr.textSize, + android.R.attr.typeface, + android.R.attr.textStyle, + android.R.attr.password, + android.R.attr.lineSpacingExtra, + android.R.attr.lineSpacingMultiplier, + android.R.attr.inputType, + android.R.attr.imeOptions, + android.R.attr.imeActionLabel, + android.R.attr.imeActionId, + android.R.attr.privateImeOptions, + android.R.attr.textSelectHandleLeft, + android.R.attr.textSelectHandleRight, + android.R.attr.textSelectHandle, + android.R.attr.textIsSelectable, + android.R.attr.textAllCaps + }; + + // The array must be sorted in ascending order, otherwise obtainStyledAttributes() + // might fail to find some attributes + Arrays.sort(attributes); + TypedArray array = ctx.obtainStyledAttributes(typedValue.data, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + textSize = array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.textSize), textSize); + styleIndex = array.getInt(sortedAttrs.indexOf(android.R.attr.textStyle), styleIndex); + typefaceIndex = array.getInt(sortedAttrs.indexOf(android.R.attr.typeface), typefaceIndex); + textColorHighlight = array.getColor(sortedAttrs.indexOf(android.R.attr.textColorHighlight), textColorHighlight); + allCaps = array.getBoolean(sortedAttrs.indexOf(android.R.attr.textAllCaps), allCaps); + + ColorStateList textColor = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColor)); + ColorStateList textColorHint = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColorHint)); + ColorStateList textColorLink = array.getColorStateList(sortedAttrs.indexOf(android.R.attr.textColorLink)); + + json.put("TextAppearance_textSize", textSize); + json.put("TextAppearance_textStyle", styleIndex); + json.put("TextAppearance_typeface", typefaceIndex); + json.put("TextAppearance_textColorHighlight", textColorHighlight); + json.put("TextAppearance_textAllCaps", allCaps); + if (textColor != null) + json.put("TextAppearance_textColor", getColorStateList(textColor)); + if (textColorHint != null) + json.put("TextAppearance_textColorHint", getColorStateList(textColorHint)); + if (textColorLink != null) + json.put("TextAppearance_textColorLink", getColorStateList(textColorLink)); + + json.put("TextView_editable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.editable), false)); + json.put("TextView_inputMethod", array.getText(sortedAttrs.indexOf(android.R.attr.inputMethod))); + json.put("TextView_numeric", array.getInt(sortedAttrs.indexOf(android.R.attr.numeric), 0)); + json.put("TextView_digits", array.getText(sortedAttrs.indexOf(android.R.attr.digits))); + json.put("TextView_phoneNumber", array.getBoolean(sortedAttrs.indexOf(android.R.attr.phoneNumber), false)); + json.put("TextView_autoText", array.getBoolean(sortedAttrs.indexOf(android.R.attr.autoText), false)); + json.put("TextView_capitalize", array.getInt(sortedAttrs.indexOf(android.R.attr.capitalize), -1)); + json.put("TextView_bufferType", array.getInt(sortedAttrs.indexOf(android.R.attr.bufferType), 0)); + json.put("TextView_selectAllOnFocus", array.getBoolean(sortedAttrs.indexOf(android.R.attr.selectAllOnFocus), false)); + json.put("TextView_autoLink", array.getInt(sortedAttrs.indexOf(android.R.attr.autoLink), 0)); + json.put("TextView_linksClickable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.linksClickable), true)); + json.put("TextView_drawableLeft", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableLeft)), styleName + "_TextView_drawableLeft", null)); + json.put("TextView_drawableTop", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableTop)), styleName + "_TextView_drawableTop", null)); + json.put("TextView_drawableRight", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableRight)), styleName + "_TextView_drawableRight", null)); + json.put("TextView_drawableBottom", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableBottom)), styleName + "_TextView_drawableBottom", null)); + json.put("TextView_drawableStart", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableStart)), styleName + "_TextView_drawableStart", null)); + json.put("TextView_drawableEnd", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.drawableEnd)), styleName + "_TextView_drawableEnd", null)); + json.put("TextView_maxLines", array.getInt(sortedAttrs.indexOf(android.R.attr.maxLines), -1)); + json.put("TextView_drawablePadding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.drawablePadding), 0)); + + try { + json.put("TextView_textCursorDrawable", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.textCursorDrawable)), styleName + "_TextView_textCursorDrawable", null)); + } catch (Exception e_) { + json.put("TextView_textCursorDrawable", getDrawable(m_context.getResources().getDrawable(array.getResourceId(sortedAttrs.indexOf(android.R.attr.textCursorDrawable), 0), m_theme), styleName + "_TextView_textCursorDrawable", null)); + } + + json.put("TextView_maxLines", array.getInt(sortedAttrs.indexOf(android.R.attr.maxLines), -1)); + json.put("TextView_maxHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxHeight), -1)); + json.put("TextView_lines", array.getInt(sortedAttrs.indexOf(android.R.attr.lines), -1)); + json.put("TextView_height", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.height), -1)); + json.put("TextView_minLines", array.getInt(sortedAttrs.indexOf(android.R.attr.minLines), -1)); + json.put("TextView_minHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minHeight), -1)); + json.put("TextView_maxEms", array.getInt(sortedAttrs.indexOf(android.R.attr.maxEms), -1)); + json.put("TextView_maxWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxWidth), -1)); + json.put("TextView_ems", array.getInt(sortedAttrs.indexOf(android.R.attr.ems), -1)); + json.put("TextView_width", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.width), -1)); + json.put("TextView_minEms", array.getInt(sortedAttrs.indexOf(android.R.attr.minEms), -1)); + json.put("TextView_minWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minWidth), -1)); + json.put("TextView_gravity", array.getInt(sortedAttrs.indexOf(android.R.attr.gravity), -1)); + json.put("TextView_hint", array.getText(sortedAttrs.indexOf(android.R.attr.hint))); + json.put("TextView_text", array.getText(sortedAttrs.indexOf(android.R.attr.text))); + json.put("TextView_scrollHorizontally", array.getBoolean(sortedAttrs.indexOf(android.R.attr.scrollHorizontally), false)); + json.put("TextView_singleLine", array.getBoolean(sortedAttrs.indexOf(android.R.attr.singleLine), false)); + json.put("TextView_ellipsize", array.getInt(sortedAttrs.indexOf(android.R.attr.ellipsize), -1)); + json.put("TextView_marqueeRepeatLimit", array.getInt(sortedAttrs.indexOf(android.R.attr.marqueeRepeatLimit), 3)); + json.put("TextView_includeFontPadding", array.getBoolean(sortedAttrs.indexOf(android.R.attr.includeFontPadding), true)); + json.put("TextView_cursorVisible", array.getBoolean(sortedAttrs.indexOf(android.R.attr.maxLength), true)); + json.put("TextView_maxLength", array.getInt(sortedAttrs.indexOf(android.R.attr.maxLength), -1)); + json.put("TextView_textScaleX", array.getFloat(sortedAttrs.indexOf(android.R.attr.textScaleX), 1.0f)); + json.put("TextView_freezesText", array.getBoolean(sortedAttrs.indexOf(android.R.attr.freezesText), false)); + json.put("TextView_shadowColor", array.getInt(sortedAttrs.indexOf(android.R.attr.shadowColor), 0)); + json.put("TextView_shadowDx", array.getFloat(sortedAttrs.indexOf(android.R.attr.shadowDx), 0)); + json.put("TextView_shadowDy", array.getFloat(sortedAttrs.indexOf(android.R.attr.shadowDy), 0)); + json.put("TextView_shadowRadius", array.getFloat(sortedAttrs.indexOf(android.R.attr.shadowRadius), 0)); + json.put("TextView_enabled", array.getBoolean(sortedAttrs.indexOf(android.R.attr.enabled), true)); + json.put("TextView_password", array.getBoolean(sortedAttrs.indexOf(android.R.attr.password), false)); + json.put("TextView_lineSpacingExtra", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.lineSpacingExtra), 0)); + json.put("TextView_lineSpacingMultiplier", array.getFloat(sortedAttrs.indexOf(android.R.attr.lineSpacingMultiplier), 1.0f)); + json.put("TextView_inputType", array.getInt(sortedAttrs.indexOf(android.R.attr.inputType), EditorInfo.TYPE_NULL)); + json.put("TextView_imeOptions", array.getInt(sortedAttrs.indexOf(android.R.attr.imeOptions), EditorInfo.IME_NULL)); + json.put("TextView_imeActionLabel", array.getText(sortedAttrs.indexOf(android.R.attr.imeActionLabel))); + json.put("TextView_imeActionId", array.getInt(sortedAttrs.indexOf(android.R.attr.imeActionId), 0)); + json.put("TextView_privateImeOptions", array.getString(sortedAttrs.indexOf(android.R.attr.privateImeOptions))); + + try { + json.put("TextView_textSelectHandleLeft", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.textSelectHandleLeft)), styleName + "_TextView_textSelectHandleLeft", null)); + } catch (Exception _e) { + json.put("TextView_textSelectHandleLeft", getDrawable(m_context.getResources().getDrawable(array.getResourceId(sortedAttrs.indexOf(android.R.attr.textSelectHandleLeft), 0), m_theme), styleName + "_TextView_textSelectHandleLeft", null)); + } + + try { + json.put("TextView_textSelectHandleRight", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.textSelectHandleRight)), styleName + "_TextView_textSelectHandleRight", null)); + } catch (Exception _e) { + json.put("TextView_textSelectHandleRight", getDrawable(m_context.getResources().getDrawable(array.getResourceId(sortedAttrs.indexOf(android.R.attr.textSelectHandleRight), 0), m_theme), styleName + "_TextView_textSelectHandleRight", null)); + } + + try { + json.put("TextView_textSelectHandle", getDrawable(array.getDrawable(sortedAttrs.indexOf(android.R.attr.textSelectHandle)), styleName + "_TextView_textSelectHandle", null)); + } catch (Exception _e) { + json.put("TextView_textSelectHandle", getDrawable(m_context.getResources().getDrawable(array.getResourceId(sortedAttrs.indexOf(android.R.attr.textSelectHandle), 0), m_theme), styleName + "_TextView_textSelectHandle", null)); + } + json.put("TextView_textIsSelectable", array.getBoolean(sortedAttrs.indexOf(android.R.attr.textIsSelectable), false)); + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + public JSONObject extractImageViewInformation(int styleName, String qtClassName) { + JSONObject json = new JSONObject(); + try { + extractViewInformation(styleName, json, qtClassName); + + int[] attributes = new int[]{ + android.R.attr.src, + android.R.attr.baselineAlignBottom, + android.R.attr.adjustViewBounds, + android.R.attr.maxWidth, + android.R.attr.maxHeight, + android.R.attr.scaleType, + android.R.attr.cropToPadding, + android.R.attr.tint + + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(styleName, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable drawable = array.getDrawable(sortedAttrs.indexOf(android.R.attr.src)); + if (drawable != null) + json.put("ImageView_src", getDrawable(drawable, styleName + "_ImageView_src", null)); + + json.put("ImageView_baselineAlignBottom", array.getBoolean(sortedAttrs.indexOf(android.R.attr.baselineAlignBottom), false)); + json.put("ImageView_adjustViewBounds", array.getBoolean(sortedAttrs.indexOf(android.R.attr.baselineAlignBottom), false)); + json.put("ImageView_maxWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxWidth), Integer.MAX_VALUE)); + json.put("ImageView_maxHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxHeight), Integer.MAX_VALUE)); + int index = array.getInt(sortedAttrs.indexOf(android.R.attr.scaleType), -1); + if (index >= 0) + json.put("ImageView_scaleType", sScaleTypeArray[index]); + + int tint = array.getInt(sortedAttrs.indexOf(android.R.attr.tint), 0); + if (tint != 0) + json.put("ImageView_tint", tint); + + json.put("ImageView_cropToPadding", array.getBoolean(sortedAttrs.indexOf(android.R.attr.cropToPadding), false)); + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + void extractCompoundButton(SimpleJsonWriter jsonWriter, int styleName, String className, String qtClass) { + JSONObject json = extractTextAppearanceInformation(styleName, qtClass); + + TypedValue typedValue = new TypedValue(); + Context ctx = new ContextThemeWrapper(m_context, m_theme); + ctx.getTheme().resolveAttribute(styleName, typedValue, true); + final int[] attributes = new int[]{android.R.attr.button}; + TypedArray array = ctx.obtainStyledAttributes(typedValue.data, attributes); + Drawable drawable = array.getDrawable(0); + array.recycle(); + + try { + if (drawable != null) + json.put("CompoundButton_button", getDrawable(drawable, styleName + "_CompoundButton_button", null)); + jsonWriter.name(className).value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractProgressBarInfo(JSONObject json, int styleName) { + try { + final int[] attributes = new int[]{ + android.R.attr.minWidth, + android.R.attr.maxWidth, + android.R.attr.minHeight, + android.R.attr.maxHeight, + android.R.attr.indeterminateDuration, + android.R.attr.progressDrawable, + android.R.attr.indeterminateDrawable + }; + + // The array must be sorted in ascending order, otherwise obtainStyledAttributes() + // might fail to find some attributes + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(styleName, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + json.put("ProgressBar_indeterminateDuration", array.getInt(sortedAttrs.indexOf(android.R.attr.indeterminateDuration), 4000)); + json.put("ProgressBar_minWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minWidth), 24)); + json.put("ProgressBar_maxWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxWidth), 48)); + json.put("ProgressBar_minHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.minHeight), 24)); + json.put("ProgressBar_maxHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.maxHeight), 28)); + json.put("ProgressBar_progress_id", android.R.id.progress); + json.put("ProgressBar_secondaryProgress_id", android.R.id.secondaryProgress); + + Drawable drawable = array.getDrawable(sortedAttrs.indexOf(android.R.attr.progressDrawable)); + if (drawable != null) + json.put("ProgressBar_progressDrawable", getDrawable(drawable, + styleName + "_ProgressBar_progressDrawable", null)); + + drawable = array.getDrawable(sortedAttrs.indexOf(android.R.attr.indeterminateDrawable)); + if (drawable != null) + json.put("ProgressBar_indeterminateDrawable", getDrawable(drawable, + styleName + "_ProgressBar_indeterminateDrawable", null)); + + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractProgressBar(SimpleJsonWriter writer, int styleName, String className, String qtClass) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.progressBarStyle, qtClass); + try { + extractProgressBarInfo(json, styleName); + writer.name(className).value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractAbsSeekBar(SimpleJsonWriter jsonWriter) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.seekBarStyle, "QSlider"); + extractProgressBarInfo(json, android.R.attr.seekBarStyle); + try { + int[] attributes = new int[]{ + android.R.attr.thumb, + android.R.attr.thumbOffset + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.seekBarStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.thumb)); + if (d != null) + json.put("SeekBar_thumb", getDrawable(d, android.R.attr.seekBarStyle + "_SeekBar_thumb", null)); + json.put("SeekBar_thumbOffset", array.getDimensionPixelOffset(sortedAttrs.indexOf(android.R.attr.thumbOffset), -1)); + array.recycle(); + jsonWriter.name("seekBarStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractSwitch(SimpleJsonWriter jsonWriter) { + JSONObject json = new JSONObject(); + try { + int[] attributes = new int[]{ + android.R.attr.thumb, + android.R.attr.track, + android.R.attr.switchTextAppearance, + android.R.attr.textOn, + android.R.attr.textOff, + android.R.attr.switchMinWidth, + android.R.attr.switchPadding, + android.R.attr.thumbTextPadding, + android.R.attr.showText, + android.R.attr.splitTrack + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.switchStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable thumb = array.getDrawable(sortedAttrs.indexOf(android.R.attr.thumb)); + if (thumb != null) + json.put("Switch_thumb", getDrawable(thumb, android.R.attr.switchStyle + "_Switch_thumb", null)); + + Drawable track = array.getDrawable(sortedAttrs.indexOf(android.R.attr.track)); + if (track != null) + json.put("Switch_track", getDrawable(track, android.R.attr.switchStyle + "_Switch_track", null)); + + json.put("Switch_textOn", array.getText(sortedAttrs.indexOf(android.R.attr.textOn))); + json.put("Switch_textOff", array.getText(sortedAttrs.indexOf(android.R.attr.textOff))); + json.put("Switch_switchMinWidth", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.switchMinWidth), 0)); + json.put("Switch_switchPadding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.switchPadding), 0)); + json.put("Switch_thumbTextPadding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.thumbTextPadding), 0)); + json.put("Switch_showText", array.getBoolean(sortedAttrs.indexOf(android.R.attr.showText), true)); + json.put("Switch_splitTrack", array.getBoolean(sortedAttrs.indexOf(android.R.attr.splitTrack), false)); + + // Get textAppearance values + final int textAppearanceId = array.getResourceId(sortedAttrs.indexOf(android.R.attr.switchTextAppearance), -1); + json.put("Switch_switchTextAppearance", extractTextAppearance(textAppearanceId, true)); + + array.recycle(); + jsonWriter.name("switchStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + JSONObject extractCheckedTextView(String itemName) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.checkedTextViewStyle, itemName); + try { + int[] attributes = new int[]{ + android.R.attr.checkMark, + }; + + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.switchStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable drawable = array.getDrawable(sortedAttrs.indexOf(android.R.attr.checkMark)); + if (drawable != null) + json.put("CheckedTextView_checkMark", getDrawable(drawable, itemName + "_CheckedTextView_checkMark", null)); + array.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + private JSONObject extractItemStyle(int resourceId, String itemName) + { + try { + XmlResourceParser parser = m_context.getResources().getLayout(resourceId); + int type = parser.next(); + while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) + type = parser.next(); + + if (type != XmlPullParser.START_TAG) + return null; + + AttributeSet attributes = Xml.asAttributeSet(parser); + String name = parser.getName(); + if (name.equals("TextView")) + return extractTextAppearanceInformation(android.R.attr.textViewStyle, itemName, android.R.attr.textAppearanceListItem, attributes); + else if (name.equals("CheckedTextView")) + return extractCheckedTextView(itemName); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private void extractItemsStyle(SimpleJsonWriter jsonWriter) { + try { + JSONObject itemStyle = extractItemStyle(android.R.layout.simple_list_item_1, "simple_list_item"); + if (itemStyle != null) + jsonWriter.name("simple_list_item").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_list_item_checked, "simple_list_item_checked"); + if (itemStyle != null) + jsonWriter.name("simple_list_item_checked").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_list_item_multiple_choice, "simple_list_item_multiple_choice"); + if (itemStyle != null) + jsonWriter.name("simple_list_item_multiple_choice").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_list_item_single_choice, "simple_list_item_single_choice"); + if (itemStyle != null) + jsonWriter.name("simple_list_item_single_choice").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_spinner_item, "simple_spinner_item"); + if (itemStyle != null) + jsonWriter.name("simple_spinner_item").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_spinner_dropdown_item, "simple_spinner_dropdown_item"); + if (itemStyle != null) + jsonWriter.name("simple_spinner_dropdown_item").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_dropdown_item_1line, "simple_dropdown_item_1line"); + if (itemStyle != null) + jsonWriter.name("simple_dropdown_item_1line").value(itemStyle); + itemStyle = extractItemStyle(android.R.layout.simple_selectable_list_item, "simple_selectable_list_item"); + if (itemStyle != null) + jsonWriter.name("simple_selectable_list_item").value(itemStyle); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractListView(SimpleJsonWriter writer) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.listViewStyle, "QListView"); + try { + int[] attributes = new int[]{ + android.R.attr.divider, + android.R.attr.dividerHeight + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.listViewStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable divider = array.getDrawable(sortedAttrs.indexOf(android.R.attr.divider)); + if (divider != null) + json.put("ListView_divider", getDrawable(divider, android.R.attr.listViewStyle + "_ListView_divider", null)); + + json.put("ListView_dividerHeight", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.dividerHeight), 0)); + + array.recycle(); + writer.name("listViewStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractCalendar(SimpleJsonWriter writer) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.calendarViewStyle, "QCalendarWidget"); + try { + int[] attributes = new int[]{ + android.R.attr.firstDayOfWeek, + android.R.attr.focusedMonthDateColor, + android.R.attr.selectedWeekBackgroundColor, + android.R.attr.showWeekNumber, + android.R.attr.shownWeekCount, + android.R.attr.unfocusedMonthDateColor, + android.R.attr.weekNumberColor, + android.R.attr.weekSeparatorLineColor, + android.R.attr.selectedDateVerticalBar, + android.R.attr.dateTextAppearance, + android.R.attr.weekDayTextAppearance + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.calendarViewStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.selectedDateVerticalBar)); + if (d != null) + json.put("CalendarView_selectedDateVerticalBar", getDrawable(d, android.R.attr.calendarViewStyle + "_CalendarView_selectedDateVerticalBar", null)); + + int textAppearanceId = array.getResourceId(sortedAttrs.indexOf(android.R.attr.dateTextAppearance), -1); + json.put("CalendarView_dateTextAppearance", extractTextAppearance(textAppearanceId, true)); + textAppearanceId = array.getResourceId(sortedAttrs.indexOf(android.R.attr.weekDayTextAppearance), -1); + json.put("CalendarView_weekDayTextAppearance", extractTextAppearance(textAppearanceId, true)); + + + json.put("CalendarView_firstDayOfWeek", array.getInt(sortedAttrs.indexOf(android.R.attr.firstDayOfWeek), 0)); + json.put("CalendarView_focusedMonthDateColor", array.getColor(sortedAttrs.indexOf(android.R.attr.focusedMonthDateColor), 0)); + json.put("CalendarView_selectedWeekBackgroundColor", array.getColor(sortedAttrs.indexOf(android.R.attr.selectedWeekBackgroundColor), 0)); + json.put("CalendarView_showWeekNumber", array.getBoolean(sortedAttrs.indexOf(android.R.attr.showWeekNumber), true)); + json.put("CalendarView_shownWeekCount", array.getInt(sortedAttrs.indexOf(android.R.attr.shownWeekCount), 6)); + json.put("CalendarView_unfocusedMonthDateColor", array.getColor(sortedAttrs.indexOf(android.R.attr.unfocusedMonthDateColor), 0)); + json.put("CalendarView_weekNumberColor", array.getColor(sortedAttrs.indexOf(android.R.attr.weekNumberColor), 0)); + json.put("CalendarView_weekSeparatorLineColor", array.getColor(sortedAttrs.indexOf(android.R.attr.weekSeparatorLineColor), 0)); + array.recycle(); + writer.name("calendarViewStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractToolBar(SimpleJsonWriter writer) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.toolbarStyle, "QToolBar"); + try { + int[] attributes = new int[]{ + android.R.attr.background, + android.R.attr.backgroundStacked, + android.R.attr.backgroundSplit, + android.R.attr.divider, + android.R.attr.itemPadding + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.toolbarStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.background)); + if (d != null) + json.put("ActionBar_background", getDrawable(d, android.R.attr.toolbarStyle + "_ActionBar_background", null)); + + d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.backgroundStacked)); + if (d != null) + json.put("ActionBar_backgroundStacked", getDrawable(d, android.R.attr.toolbarStyle + "_ActionBar_backgroundStacked", null)); + + d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.backgroundSplit)); + if (d != null) + json.put("ActionBar_backgroundSplit", getDrawable(d, android.R.attr.toolbarStyle + "_ActionBar_backgroundSplit", null)); + + d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.divider)); + if (d != null) + json.put("ActionBar_divider", getDrawable(d, android.R.attr.toolbarStyle + "_ActionBar_divider", null)); + + json.put("ActionBar_itemPadding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.itemPadding), 0)); + + array.recycle(); + writer.name("actionBarStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + void extractTabBar(SimpleJsonWriter writer) { + JSONObject json = extractTextAppearanceInformation(android.R.attr.actionBarTabBarStyle, "QTabBar"); + try { + int[] attributes = new int[]{ + android.R.attr.showDividers, + android.R.attr.dividerPadding, + android.R.attr.divider + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.actionBarTabStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable d = array.getDrawable(sortedAttrs.indexOf(android.R.attr.divider)); + if (d != null) + json.put("LinearLayout_divider", getDrawable(d, android.R.attr.actionBarTabStyle + "_LinearLayout_divider", null)); + json.put("LinearLayout_showDividers", array.getInt(sortedAttrs.indexOf(android.R.attr.showDividers), 0)); + json.put("LinearLayout_dividerPadding", array.getDimensionPixelSize(sortedAttrs.indexOf(android.R.attr.dividerPadding), 0)); + + array.recycle(); + writer.name("actionBarTabBarStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void extractWindow(SimpleJsonWriter writer) { + JSONObject json = new JSONObject(); + try { + int[] attributes = new int[]{ + android.R.attr.windowBackground, + android.R.attr.windowFrame + }; + Arrays.sort(attributes); + TypedArray array = obtainStyledAttributes(android.R.attr.popupWindowStyle, attributes); + ArrayList<Integer> sortedAttrs = getArrayListFromIntArray(attributes); + + Drawable background = array.getDrawable(sortedAttrs.indexOf(android.R.attr.windowBackground)); + if (background != null) + json.put("Window_windowBackground", getDrawable(background, android.R.attr.popupWindowStyle + "_Window_windowBackground", null)); + + Drawable frame = array.getDrawable(sortedAttrs.indexOf(android.R.attr.windowFrame)); + if (frame != null) + json.put("Window_windowFrame", getDrawable(frame, android.R.attr.popupWindowStyle + "_Window_windowFrame", null)); + array.recycle(); + writer.name("windowStyle").value(json); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private JSONObject extractDefaultPalette() { + JSONObject json = extractTextAppearance(android.R.attr.textAppearance); + try { + json.put("defaultBackgroundColor", defaultBackgroundColor); + json.put("defaultTextColorPrimary", defaultTextColor); + } catch (Exception e) { + e.printStackTrace(); + } + return json; + } + + static class SimpleJsonWriter { + private final OutputStreamWriter m_writer; + private boolean m_addComma = false; + private int m_indentLevel = 0; + + public SimpleJsonWriter(String filePath) throws FileNotFoundException { + m_writer = new OutputStreamWriter(new FileOutputStream(filePath)); + } + + public void close() throws IOException { + m_writer.close(); + } + + private void writeIndent() throws IOException { + m_writer.write(" ", 0, m_indentLevel); + } + + void beginObject() throws IOException { + writeIndent(); + m_writer.write("{\n"); + ++m_indentLevel; + m_addComma = false; + } + + void endObject() throws IOException { + m_writer.write("\n"); + writeIndent(); + m_writer.write("}\n"); + --m_indentLevel; + m_addComma = false; + } + + SimpleJsonWriter name(String name) throws IOException { + if (m_addComma) { + m_writer.write(",\n"); + } + writeIndent(); + m_writer.write(JSONObject.quote(name) + ": "); + m_addComma = true; + return this; + } + + void value(JSONObject value) throws IOException { + m_writer.write(value.toString()); + } + } + + static class DrawableCache { + JSONObject object; + Object drawable; + public DrawableCache(JSONObject json, Object drawable) { + object = json; + this.drawable = drawable; + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityDelegate.java new file mode 100644 index 0000000000..d23c87e792 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtAccessibilityDelegate.java @@ -0,0 +1,523 @@ +// 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.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.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +class QtAccessibilityDelegate extends View.AccessibilityDelegate +{ + private static final String TAG = "Qt A11Y"; + + // 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 + + // The platform might ask for the class implementing the "view". + // Pretend to be an inner class of the QtSurface. + private static final String DEFAULT_CLASS_NAME = "$VirtualChild"; + + private View m_view = null; + 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. + private int m_focusedVirtualViewId = INVALID_ID; + // When exploring the screen by touch, the item "hovered" by the finger. + private int m_hoveredVirtualViewId = INVALID_ID; + + // Cache coordinates of the view to know the global offset + // this is because the Android platform window does not take + // the offset of the view on screen into account (eg status bar on top) + private final int[] m_globalOffset = new int[2]; + private int m_oldOffsetX = 0; + private int m_oldOffsetY = 0; + + private class HoverEventListener implements View.OnHoverListener + { + @Override + public boolean onHover(View v, MotionEvent event) + { + return dispatchHoverEvent(event); + } + } + // TODO do we want to have one QtAccessibilityDelegate for the whole app (QtRootLayout) or + // e.g. one per window? + public QtAccessibilityDelegate(QtLayout layout) + { + m_layout = layout; + + m_manager = (AccessibilityManager) m_layout.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (m_manager != null) { + AccessibilityManagerListener accServiceListener = new AccessibilityManagerListener(); + if (!m_manager.addAccessibilityStateChangeListener(accServiceListener)) + Log.w("Qt A11y", "Could not register a11y state change listener"); + if (m_manager.isEnabled()) + accServiceListener.onAccessibilityStateChanged(true); + } + } + + private class AccessibilityManagerListener implements AccessibilityManager.AccessibilityStateChangeListener + { + @Override + public void onAccessibilityStateChanged(boolean enabled) + { + if (Os.getenv("QT_ANDROID_DISABLE_ACCESSIBILITY") != null) + return; + if (enabled) { + try { + View view = m_view; + if (view == null) { + 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) + //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_layout.getChildCount(), + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + m_view = view; + + m_view.setOnHoverListener(new HoverEventListener()); + } catch (Exception e) { + // Unknown exception means something went wrong. + Log.w("Qt A11y", "Unknown exception: " + e); + } + } else { + if (m_view != null) { + m_layout.removeView(m_view); + m_view = null; + } + } + + QtNativeAccessibility.setActive(enabled); + } + } + + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) + { + return m_nodeProvider; + } + + // For "explore by touch" we need all movement events here first + // (user moves finger over screen to discover items on screen). + private boolean dispatchHoverEvent(MotionEvent event) + { + if (!m_manager.isTouchExplorationEnabled()) { + return false; + } + + int virtualViewId = QtNativeAccessibility.hitTest(event.getX(), event.getY()); + if (virtualViewId == INVALID_ID) { + virtualViewId = View.NO_ID; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: + setHoveredVirtualViewId(virtualViewId); + break; + } + + return true; + } + + public void notifyScrolledEvent(int viewId) + { + QtNative.runAction(() -> sendEventForVirtualViewId(viewId, + AccessibilityEvent.TYPE_VIEW_SCROLLED)); + } + + public void notifyLocationChange(int viewId) + { + QtNative.runAction(() -> { + if (m_focusedVirtualViewId == viewId) + invalidateVirtualViewId(m_focusedVirtualViewId); + }); + } + + public void notifyObjectHide(int viewId, int 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) + { + 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) + { + 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; + } + + 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); + + event.setEnabled(true); + event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); + + event.setContentDescription(value); + + 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); + + if (!group.requestSendAccessibilityEvent(m_view, event)) + Log.w(TAG, "Failed to send value change announcement for " + event.getClassName()); + }); + } + + public void sendEventForVirtualViewId(int virtualViewId, int eventType) + { + final AccessibilityEvent event = getEventForVirtualViewId(virtualViewId, eventType); + sendAccessibilityEvent(event); + } + + public void sendAccessibilityEvent(AccessibilityEvent event) + { + if (event == null) + 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; + } + + group.requestSendAccessibilityEvent(m_view, event); + } + + public void invalidateVirtualViewId(int virtualViewId) + { + final AccessibilityEvent event = getEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + + if (event == null) + return; + + event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + sendAccessibilityEvent(event); + } + + private void setHoveredVirtualViewId(int virtualViewId) + { + if (m_hoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = m_hoveredVirtualViewId; + m_hoveredVirtualViewId = virtualViewId; + sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualViewId(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + + private AccessibilityEvent getEventForVirtualViewId(int virtualViewId, int eventType) + { + if ((virtualViewId == INVALID_ID) || !m_manager.isEnabled()) { + Log.w(TAG, "getEventForVirtualViewId for invalid view"); + return null; + } + + if (m_layout.getChildCount() == 0) + return null; + + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + + event.setEnabled(true); + event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); + + event.setContentDescription(QtNativeAccessibility.descriptionForAccessibleObject(virtualViewId)); + if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) + Log.w(TAG, "AccessibilityEvent with empty description"); + + event.setPackageName(m_view.getContext().getPackageName()); + event.setSource(m_view, virtualViewId); + 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 id : ids) { + Log.i(TAG, parentId + " has child: " + id); + dumpNodes(id); + } + } + + private AccessibilityNodeInfo getNodeForView() + { + // Since we don't want the parent to be focusable, but we can't remove + // actions from a node, copy over the necessary fields. + final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(m_view); + final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(m_view); + m_view.onInitializeAccessibilityNodeInfo(source); + + // Get the actual position on screen, taking the status bar into account. + m_view.getLocationOnScreen(m_globalOffset); + final int offsetX = m_globalOffset[0]; + final int offsetY = m_globalOffset[1]; + + // Copy over parent and screen bounds. + final Rect m_tempParentRect = new Rect(); + source.getBoundsInParent(m_tempParentRect); + result.setBoundsInParent(m_tempParentRect); + + final Rect m_tempScreenRect = new Rect(); + source.getBoundsInScreen(m_tempScreenRect); + m_tempScreenRect.offset(offsetX, offsetY); + result.setBoundsInScreen(m_tempScreenRect); + + // Set up the parent view, if applicable. + final ViewParent parent = m_view.getParent(); + if (parent instanceof View) { + result.setParent((View) parent); + } + + result.setVisibleToUser(source.isVisibleToUser()); + result.setPackageName(source.getPackageName()); + result.setClassName(source.getClassName()); + + // Spit out the entire hierarchy for debugging purposes + // dumpNodes(-1); + + if (m_layout.getChildCount() != 0) { + int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(-1); + for (int id : ids) + result.addChild(m_view, id); + } + + // The offset values have changed, so we need to re-focus the + // currently focused item, otherwise it will have an incorrect + // focus frame + if ((m_oldOffsetX != offsetX) || (m_oldOffsetY != offsetY)) { + m_oldOffsetX = offsetX; + m_oldOffsetY = offsetY; + if (m_focusedVirtualViewId != INVALID_ID) { + m_nodeProvider.performAction(m_focusedVirtualViewId, + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, + new Bundle()); + m_nodeProvider.performAction(m_focusedVirtualViewId, + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, + new Bundle()); + } + } + + return result; + } + + private AccessibilityNodeInfo getNodeForVirtualViewId(int virtualViewId) + { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); + + node.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); + node.setPackageName(m_view.getContext().getPackageName()); + + if (m_layout.getChildCount() == 0 || !QtNativeAccessibility.populateNode(virtualViewId, node)) { + return node; + } + + // set only if valid, otherwise we return a node that is invalid and will crash when accessed + node.setSource(m_view, virtualViewId); + + if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) + Log.w(TAG, "AccessibilityNodeInfo with empty contentDescription: " + virtualViewId); + + int parentId = QtNativeAccessibility.parentId(virtualViewId); + node.setParent(m_view, parentId); + + Rect screenRect = QtNativeAccessibility.screenRect(virtualViewId); + final int offsetX = m_globalOffset[0]; + final int offsetY = m_globalOffset[1]; + screenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(screenRect); + + Rect parentScreenRect = QtNativeAccessibility.screenRect(parentId); + screenRect.offset(-parentScreenRect.left, -parentScreenRect.top); + node.setBoundsInParent(screenRect); + + // Manage internal accessibility focus state. + if (m_focusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); + } + + int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(virtualViewId); + 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)); + } else { + node.setCollectionInfo(CollectionInfo.obtain(ids.length, 1, false)); + } + } + + return node; + } + + private final AccessibilityNodeProvider m_nodeProvider = new AccessibilityNodeProvider() + { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) + { + if (virtualViewId == View.NO_ID || m_layout.getChildCount() == 0) { + return getNodeForView(); + } + return getNodeForVirtualViewId(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) + { + boolean handled = false; + //Log.i(TAG, "PERFORM ACTION: " + action + " on " + virtualViewId); + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + // Only handle the FOCUS action if it's placing focus on + // a different view that was previously focused. + if (m_focusedVirtualViewId != virtualViewId) { + m_focusedVirtualViewId = virtualViewId; + m_view.invalidate(); + sendEventForVirtualViewId(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + handled = true; + } + break; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + if (m_focusedVirtualViewId == virtualViewId) { + m_focusedVirtualViewId = INVALID_ID; + } + // Since we're managing focus at the parent level, we are + // likely to receive a FOCUS action before a CLEAR_FOCUS + // action. We'll give the benefit of the doubt to the + // framework and always handle FOCUS_CLEARED. + m_view.invalidate(); + sendEventForVirtualViewId(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + handled = true; + break; + default: + // Let the node provider handle focus for the view node. + if (virtualViewId == View.NO_ID) { + return m_view.performAccessibilityAction(action, arguments); + } + } + handled |= performActionForVirtualViewId(virtualViewId, action); + + return handled; + } + }; + + protected boolean performActionForVirtualViewId(int virtualViewId, int action) + { + //noinspection CommentedOutCode + { + // Log.i(TAG, "ACTION " + action + " on " + virtualViewId); + // dumpNodes(virtualViewId); + } + boolean success = false; + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + success = QtNativeAccessibility.clickAction(virtualViewId); + if (success) + sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); + break; + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + success = QtNativeAccessibility.scrollForward(virtualViewId); + if (success) + sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED); + break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + success = QtNativeAccessibility.scrollBackward(virtualViewId); + if (success) + sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED); + break; + } + return success; + } +} 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..3cb6ba220e --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java @@ -0,0 +1,325 @@ +// 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 +{ + 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); + + handleActivityRestart(); + addReferrer(getIntent()); + + QtActivityLoader loader = new QtActivityLoader(this); + loader.appendApplicationParameters(m_applicationParams); + + loader.loadQtLibraries(); + m_delegate.startNativeApplication(loader.getApplicationParameters(), + loader.getMainLibraryPath()); + } + + @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.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) + { + 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 new file mode 100644 index 0000000000..1e1a36be3c --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java @@ -0,0 +1,417 @@ +// 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; + +class QtActivityDelegate extends QtActivityDelegateBase +{ + private static final String QtTAG = "QtActivityDelegate"; + + private QtRootLayout m_layout = null; + private ImageView m_splashScreen = null; + private boolean m_splashScreenSticky = false; + + private View m_dummyView = null; + private HashMap<Integer, View> m_nativeViews = new HashMap<Integer, View>(); + + + QtActivityDelegate(Activity activity) + { + super(activity); + + setActionBarVisibility(false); + setActivityBackgroundDrawable(); + } + + + @UsedFromNativeCode + @Override + QtLayout getQtLayout() + { + return m_layout; + } + + @UsedFromNativeCode + @Override + void setSystemUiVisibility(int systemUiVisibility) + { + QtNative.runAction(() -> { + m_displayManager.setSystemUiVisibility(systemUiVisibility); + m_layout.requestLayout(); + QtNative.updateWindow(); + }); + } + + @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); + + m_activity.setContentView(m_layout); + + return updated; + } + + @Override + void startNativeApplicationImpl(String appParams, String mainLib) + { + m_layout.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + QtNative.startApplication(appParams, mainLib); + m_layout.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + + @Override + protected void setUpLayout() + { + int orientation = m_activity.getResources().getConfiguration().orientation; + m_layout = new QtRootLayout(m_activity); + + 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); + + handleUiModeChange(m_activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK); + + Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + ? m_activity.getWindowManager().getDefaultDisplay() + : m_activity.getDisplay(); + QtDisplayManager.handleRefreshRateChanged(QtDisplayManager.getRefreshRate(display)); + + m_layout.getViewTreeObserver().addOnPreDrawListener(() -> { + if (!m_inputDelegate.isKeyboardVisible()) + 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) { + m_inputDelegate.setKeyboardVisibility(false, System.nanoTime()); + return true; + } + final int[] location = new int[2]; + m_layout.getLocationOnScreen(location); + QtInputDelegate.keyboardGeometryChanged(location[0], r.bottom - location[1], + r.width(), kbHeight); + return true; + }); + registerGlobalFocusChangeListener(m_layout); + m_inputDelegate.setEditPopupMenu(new EditPopupMenu(m_activity, m_layout)); + } + + @Override + protected void setUpSplashScreen(int orientation) + { + try { + 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"); + if (!info.metaData.containsKey(splashScreenKey)) + 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"); + + 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.setScaleType(ImageView.ScaleType.FIT_XY); + m_splashScreen.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + m_layout.addView(m_splashScreen); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected void hideSplashScreen(final int duration) + { + QtNative.runAction(() -> { + if (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); + + fadeOut.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + hideSplashScreen(0); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + }); + + m_splashScreen.startAnimation(fadeOut); + }); + } + + @UsedFromNativeCode + public void initializeAccessibility() + { + 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."); + }); + } + + @UsedFromNativeCode + public void resetOptionsMenu() + { + QtNative.runAction(() -> m_activity.invalidateOptionsMenu()); + } + + @UsedFromNativeCode + public void openOptionsMenu() + { + QtNative.runAction(() -> m_activity.openOptionsMenu()); + } + + private boolean m_contextMenuVisible = false; + + public void onCreatePopupMenu(Menu menu) + { + QtNative.fillContextMenu(menu); + m_contextMenuVisible = true; + } + + @UsedFromNativeCode + @Override + public void openContextMenu(final int x, final int y, final int w, final int h) + { + 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); + } + + @UsedFromNativeCode + public void closeContextMenu() + { + QtNative.runAction(() -> m_activity.closeContextMenu()); + } + + @Override + void setActionBarVisibility(boolean visible) + { + if (m_activity.getActionBar() == null) + return; + if (ViewConfiguration.get(m_activity).hasPermanentMenuKey() || !visible) + m_activity.getActionBar().hide(); + else + m_activity.getActionBar().show(); + } + + @UsedFromNativeCode + @Override + public void addTopLevelWindow(final QtWindow window) + { + if (window == null) + return; + + QtNative.runAction(()-> { + if (m_topLevelWindows.size() == 0) { + if (m_dummyView != null) { + m_layout.removeView(m_dummyView); + m_dummyView = null; + } + } + + window.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + m_layout.addView(window, m_topLevelWindows.size()); + m_topLevelWindows.put(window.getId(), window); + if (!m_splashScreenSticky) + hideSplashScreen(); + }); + } + + @UsedFromNativeCode + @Override + 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); + } + } + }); + } + + @UsedFromNativeCode + @Override + void bringChildToFront(final int id) + { + QtNative.runAction(() -> { + QtWindow window = m_topLevelWindows.get(id); + if (window != null) + m_layout.moveChild(window, m_topLevelWindows.size() - 1); + }); + } + + @UsedFromNativeCode + @Override + void bringChildToBack(int id) + { + QtNative.runAction(() -> { + QtWindow window = m_topLevelWindows.get(id); + if (window != null) + m_layout.moveChild(window, 0); + }); + } + + @Override + QtAccessibilityDelegate createAccessibilityDelegate() + { + if (m_layout != null) + return new QtAccessibilityDelegate(m_layout); + + Log.w(QtTAG, "Null layout, failed to initialize accessibility delegate."); + return null; + } + + private void setActivityBackgroundDrawable() + { + 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()); + } + + m_activity.getWindow().setBackgroundDrawable(backgroundDrawable); + } + + // 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) + { + QtNative.runAction(()-> { + 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); + }); + } + + // 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.runAction(() -> { + if (m_nativeViews.containsKey(id)) { + View view = m_nativeViews.get(id); + 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..6fd539d8dd --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegateBase.java @@ -0,0 +1,267 @@ +// 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 QtAccessibilityDelegate m_accessibilityDelegate = null; + 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); + abstract QtAccessibilityDelegate createAccessibilityDelegate(); + abstract QtLayout getQtLayout(); + + // With these we are okay with default implementation doing nothing + void setUpLayout() {} + void setUpSplashScreen(int orientation) {} + void hideSplashScreen(final int duration) {} + void openContextMenu(final int x, final int y, final int w, final int h) {} + void setActionBarVisibility(boolean visible) {} + void addTopLevelWindow(final QtWindow window) {} + void removeTopLevelWindow(final int id) {} + void bringChildToFront(final int id) {} + void bringChildToBack(int id) {} + void setSystemUiVisibility(int systemUiVisibility) {} + + QtActivityDelegateBase(Activity activity) + { + m_activity = activity; + // Set native context + QtNative.setActivity(m_activity); + } + + QtDisplayManager displayManager() { + return m_displayManager; + } + + @UsedFromNativeCode + 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); + } + + @UsedFromNativeCode + public void notifyLocationChange(int viewId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyLocationChange(viewId); + } + + @UsedFromNativeCode + public void notifyObjectHide(int viewId, int parentId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyObjectHide(viewId, parentId); + } + + @UsedFromNativeCode + public void notifyObjectShow(int parentId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyObjectShow(parentId); + } + + @UsedFromNativeCode + public void notifyObjectFocus(int viewId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyObjectFocus(viewId); + } + + @UsedFromNativeCode + public void notifyValueChanged(int viewId, String value) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyValueChanged(viewId, value); + } + + @UsedFromNativeCode + public void notifyScrolledEvent(int viewId) + { + if (m_accessibilityDelegate == null) + return; + m_accessibilityDelegate.notifyScrolledEvent(viewId); + } + + @UsedFromNativeCode + public void initializeAccessibility() + { + QtNative.runAction(() -> { + m_accessibilityDelegate = createAccessibilityDelegate(); + }); + } + + 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; + } + } + + @UsedFromNativeCode + public void resetOptionsMenu() + { + QtNative.runAction(() -> m_activity.invalidateOptionsMenu()); + } + + @UsedFromNativeCode + public void openOptionsMenu() + { + QtNative.runAction(() -> m_activity.openOptionsMenu()); + } + + public void onCreatePopupMenu(Menu menu) + { + QtNative.fillContextMenu(menu); + m_contextMenuVisible = true; + } + + @UsedFromNativeCode + public void closeContextMenu() + { + QtNative.runAction(() -> m_activity.closeContextMenu()); + } +} 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 new file mode 100644 index 0000000000..4524887242 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEditText.java @@ -0,0 +1,221 @@ +// 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.graphics.Canvas; +import android.text.InputType; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.KeyEvent; + +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; + 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; + } + + private void setImeOptions(int imeOptions) + { + if (m_imeOptions == imeOptions) + return; + m_imeOptions = m_imeOptions; + m_optionsChanged = true; + } + + private void setInitialCapsMode(int initialCapsMode) + { + if (m_initialCapsMode == initialCapsMode) + return; + m_initialCapsMode = initialCapsMode; + m_optionsChanged = true; + } + + + private void setInputType(int inputType) + { + if (m_inputType == inputType) + return; + m_inputType = m_inputType; + m_optionsChanged = true; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) + { + outAttrs.inputType = m_inputType; + outAttrs.imeOptions = m_imeOptions; + outAttrs.initialCapsMode = m_initialCapsMode; + m_inputConnection = new QtInputConnection(this,m_qtInputConnectionListener); + return m_inputConnection; + } + + @Override + public boolean onCheckIsTextEditor () + { + return true; + } + + @Override + public boolean onKeyDown (int keyCode, KeyEvent event) + { + 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); + } + + + 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..ff694777d5 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegate.java @@ -0,0 +1,178 @@ +// 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.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.HashMap; + +class QtEmbeddedDelegate extends QtActivityDelegateBase + implements QtNative.AppStateDetailsListener, QtEmbeddedViewInterface +{ + // 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; + + 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); + QtEmbeddedDelegateFactory.remove(m_activity); + QtNative.terminateQt(); + QtNative.setActivity(null); + QtNative.getQtThread().exit(); + } + } + }); + } + + @Override + public void onAppStateDetailsChanged(QtNative.ApplicationStateDetails details) { + synchronized (this) { + m_stateDetails = details; + } + } + + @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 + QtAccessibilityDelegate createAccessibilityDelegate() + { + // FIXME make QtAccessibilityDelegate window based or verify current way works + // also for child windows: QTBUG-120685 + return null; + } + + @UsedFromNativeCode + @Override + 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; + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java new file mode 100644 index 0000000000..8cf89e5bc3 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedDelegateFactory.java @@ -0,0 +1,37 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +package org.qtproject.qt.android; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import java.util.HashMap; + +class QtEmbeddedDelegateFactory { + private static final HashMap<Activity, QtEmbeddedDelegate> m_delegates = new HashMap<>(); + private static final Object m_delegateLock = new Object(); + + @UsedFromNativeCode + public static QtActivityDelegateBase getActivityDelegate(Activity activity) { + synchronized (m_delegateLock) { + return m_delegates.get(activity); + } + } + + public static QtEmbeddedDelegate create(Activity activity) { + synchronized (m_delegateLock) { + if (!m_delegates.containsKey(activity)) + m_delegates.put(activity, new QtEmbeddedDelegate(activity)); + + return m_delegates.get(activity); + } + } + + public static void remove(Activity activity) { + synchronized (m_delegateLock) { + m_delegates.remove(activity); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtEmbeddedLoader.java new file mode 100644 index 0000000000..0c6c4b49f0 --- /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 + QtEmbeddedDelegateFactory.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/QtInputConnection.java b/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java new file mode 100644 index 0000000000..1bfe05e7ac --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputConnection.java @@ -0,0 +1,327 @@ +// 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.os.Build; +import android.util.Log; +import android.view.WindowMetrics; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputMethodManager; +import android.view.KeyEvent; +import android.graphics.Rect; +import android.app.Activity; +import android.util.DisplayMetrics; + +class QtExtractedText +{ + public int partialEndOffset; + public int partialStartOffset; + public int selectionEnd; + public int selectionStart; + public int startOffset; + public String text; +} + +class QtNativeInputConnection +{ + static native boolean beginBatchEdit(); + static native boolean endBatchEdit(); + static native boolean commitText(String text, int newCursorPosition); + static native boolean commitCompletion(String text, int position); + static native boolean deleteSurroundingText(int leftLength, int rightLength); + static native boolean finishComposingText(); + static native int getCursorCapsMode(int reqModes); + static native QtExtractedText getExtractedText(int hintMaxChars, int hintMaxLines, int flags); + static native String getSelectedText(int flags); + static native String getTextAfterCursor(int length, int flags); + static native String getTextBeforeCursor(int length, int flags); + static native boolean setComposingText(String text, int newCursorPosition); + static native boolean setComposingRegion(int start, int end); + static native boolean setSelection(int start, int end); + static native boolean selectAll(); + static native boolean cut(); + static native boolean copy(); + static native boolean copyURL(); + static native boolean paste(); + static native boolean updateCursorPosition(); + static native void reportFullscreenMode(boolean enabled); + static native boolean fullscreenMode(); +} + +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; + private static final int ID_COPY = android.R.id.copy; + private static final int ID_PASTE = android.R.id.paste; + private static final int ID_COPY_URL = android.R.id.copyUrl; + 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 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; + } + + 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) + m_view.postDelayed(new HideKeyboardRunnable(), 100); + else + m_qtInputConnectionListener.onSetClosing(false); + } + + 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 + public boolean beginBatchEdit() + { + setClosing(false); + return QtNativeInputConnection.beginBatchEdit(); + } + + @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); + return QtNativeInputConnection.endBatchEdit(); + } + + @Override + public boolean commitCompletion(CompletionInfo text) + { + setClosing(false); + return QtNativeInputConnection.commitCompletion(text.getText().toString(), text.getPosition()); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) + { + setClosing(false); + restartImmInput(); + return QtNativeInputConnection.commitText(text.toString(), newCursorPosition); + } + + @Override + public boolean deleteSurroundingText(int leftLength, int rightLength) + { + setClosing(false); + return QtNativeInputConnection.deleteSurroundingText(leftLength, rightLength); + } + + @Override + public boolean finishComposingText() + { + // on some/all android devices hide event is not coming, but instead finishComposingText() is called twice + setClosing(true); + return QtNativeInputConnection.finishComposingText(); + } + + @Override + public int getCursorCapsMode(int reqModes) + { + return QtNativeInputConnection.getCursorCapsMode(reqModes); + } + + @Override + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) + { + QtExtractedText qExtractedText = QtNativeInputConnection.getExtractedText(request.hintMaxChars, + request.hintMaxLines, + flags); + if (qExtractedText == null) + return null; + + ExtractedText extractedText = new ExtractedText(); + extractedText.partialEndOffset = qExtractedText.partialEndOffset; + extractedText.partialStartOffset = qExtractedText.partialStartOffset; + extractedText.selectionEnd = qExtractedText.selectionEnd; + extractedText.selectionStart = qExtractedText.selectionStart; + extractedText.startOffset = qExtractedText.startOffset; + extractedText.text = qExtractedText.text; + return extractedText; + } + + public CharSequence getSelectedText(int flags) + { + return QtNativeInputConnection.getSelectedText(flags); + } + + @Override + public CharSequence getTextAfterCursor(int length, int flags) + { + return QtNativeInputConnection.getTextAfterCursor(length, flags); + } + + @Override + public CharSequence getTextBeforeCursor(int length, int flags) + { + return QtNativeInputConnection.getTextBeforeCursor(length, flags); + } + + @Override + public boolean performContextMenuAction(int id) + { + 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: + 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(); +// if (word != null) { +// Intent i = new Intent("com.android.settings.USER_DICTIONARY_INSERT"); +// i.putExtra("word", word); +// i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); +// m_view.getContext().startActivity(i); +// } + return true; + } + return super.performContextMenuAction(id); + } + + @Override + public boolean sendKeyEvent(KeyEvent event) + { + // QTBUG-85715 + // If the sendKeyEvent was invoked, it means that the button not related with composingText was used + // In such case composing text (if it exists) should be finished immediately + finishComposingText(); + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER && m_view != null) { + KeyEvent fakeEvent; + switch (m_view.m_imeOptions) { + case android.view.inputmethod.EditorInfo.IME_ACTION_NEXT: + fakeEvent = new KeyEvent(event.getDownTime(), + event.getEventTime(), + event.getAction(), + KeyEvent.KEYCODE_TAB, + event.getRepeatCount(), + event.getMetaState()); + return super.sendKeyEvent(fakeEvent); + case android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS: + fakeEvent = new KeyEvent(event.getDownTime(), + event.getEventTime(), + event.getAction(), + KeyEvent.KEYCODE_TAB, + event.getRepeatCount(), + KeyEvent.META_SHIFT_ON); + return super.sendKeyEvent(fakeEvent); + case android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION: + restartImmInput(); + break; + default: + m_qtInputConnectionListener.onSendKeyEventDefaultCase(); + break; + } + } + return super.sendKeyEvent(event); + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) + { + setClosing(false); + return QtNativeInputConnection.setComposingText(text.toString(), newCursorPosition); + } + + @Override + public boolean setComposingRegion(int start, int end) + { + setClosing(false); + return QtNativeInputConnection.setComposingRegion(start, end); + } + + @Override + public boolean setSelection(int start, int end) + { + setClosing(false); + return QtNativeInputConnection.setSelection(start, end); + } +} 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..cfa273e410 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java @@ -0,0 +1,654 @@ +// 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 { + + // 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); + } + + // 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; + } + + // Is the keyboard fully visible i.e. visible and no ongoing animation + @UsedFromNativeCode + public boolean isSoftwareKeyboardVisible() + { + return isKeyboardVisible() && !m_isKeyboardHidingAnimationOngoing; + } + + 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(); + + } + + @UsedFromNativeCode + 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); + } + + void setFocusedView(QtEditText currentEditText) + { + m_currentEditText = currentEditText; + } + + 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); + }); + } + + 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); + } + + 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; + } + } + }); + }); + } + + @UsedFromNativeCode + 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); + }); + } + + @UsedFromNativeCode + 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 + */ + @UsedFromNativeCode + 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)); + } + + 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/QtLayout.java b/src/android/jar/src/org/qtproject/qt/android/QtLayout.java new file mode 100644 index 0000000000..aedc845014 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtLayout.java @@ -0,0 +1,215 @@ +// 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.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +class QtLayout extends ViewGroup { + + public QtLayout(Context context) + { + super(context); + } + + public QtLayout(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + public QtLayout(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + 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) { + int childRight; + int childBottom; + + 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); + } + } + + // Check against minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, + * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * and with the coordinates (0, 0). + */ + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() + { + return new LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + 0, + 0); + } + + @Override + 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) { + QtLayout.LayoutParams lp = + (QtLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = lp.x; + int childTop = lp.y; + 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); + } + } + } + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) + { + return p instanceof QtLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) + { + return new LayoutParams(p); + } + + /** + * Per-child layout information associated with AbsoluteLayout. + * 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 + { + /** + * The horizontal, or X, location of the child within the view group. + */ + public int x; + /** + * The vertical, or Y, location of the child within the view group. + */ + public int y; + + /** + * Creates a new set of layout parameters with the specified width, + * height and location. + * + * @param width the width, either {@link #FILL_PARENT}, + {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #FILL_PARENT}, + {@link #WRAP_CONTENT} or a fixed size in pixels + * @param x the X location of the child + * @param y the Y location of the child + */ + public LayoutParams(int width, int height, int x, int y) + { + super(width, height); + this.x = x; + this.y = y; + } + + public LayoutParams(int width, int height) + { + super(width, height); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams source) + { + super(source); + } + } + + public void moveChild(View view, int index) + { + if (view == null) + return; + + if (indexOfChild(view) == -1) + return; + + detachViewFromParent(view); + requestLayout(); + invalidate(); + attachViewToParent(view, index, view.getLayoutParams()); + } + + /** + * set the layout params on a child view. + * <p> + * Note: This function adds the child view if it's not in the + * layout already. + */ + public void setLayoutParams(final View childView, + final ViewGroup.LayoutParams params, + final boolean forceRedraw) + { + // Invalid view + if (childView == null) + return; + + // Invalid params + if (!checkLayoutParams(params)) + return; + + // View is already in the layout and can therefore be updated + final boolean canUpdate = (this == childView.getParent()); + + if (canUpdate) { + childView.setLayoutParams(params); + if (forceRedraw) + invalidate(); + } else { + addView(childView, params); + } + } +} 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/QtMessageDialogHelper.java b/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java new file mode 100644 index 0000000000..e13abbbadd --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtMessageDialogHelper.java @@ -0,0 +1,336 @@ +// 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.Activity; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +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.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.util.ArrayList; + +class QtNativeDialogHelper +{ + static native void dialogResult(long handler, int buttonID); +} + +class ButtonStruct implements View.OnClickListener +{ + ButtonStruct(QtMessageDialogHelper dialog, int id, String text) + { + m_dialog = dialog; + m_id = id; + m_text = Html.fromHtml(text); + } + QtMessageDialogHelper m_dialog; + private final int m_id; + Spanned m_text; + + @Override + public void onClick(View view) { + QtNativeDialogHelper.dialogResult(m_dialog.handler(), m_id); + } +} + +class QtMessageDialogHelper +{ + + public QtMessageDialogHelper(Activity activity) + { + m_activity = activity; + } + + @UsedFromNativeCode + public void setStandardIcon(int icon) + { + m_standardIcon = icon; + + } + + private Drawable getIconDrawable() + { + if (m_standardIcon == 0) + return null; + + // Information, Warning, Critical, Question + switch (m_standardIcon) + { + case 1: // Information + return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_info, + m_activity.getTheme()); + case 2: // Warning + return m_activity.getResources().getDrawable(android.R.drawable.stat_sys_warning, + m_activity.getTheme()); + case 3: // Critical + return m_activity.getResources().getDrawable(android.R.drawable.ic_dialog_alert, + m_activity.getTheme()); + case 4: // Question + 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<>(); + m_buttonsList.add(new ButtonStruct(this, id, text)); + } + + private Drawable getStyledDrawable(int id) + { + 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(() -> { + 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; + } + + 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; + } + + 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_buttonsList != null) + { + LinearLayout buttonsLayout = new LinearLayout(m_activity); + buttonsLayout.setOrientation(LinearLayout.HORIZONTAL); + buttonsLayout.setId(id++); + boolean firstButton = true; + for (ButtonStruct button: m_buttonsList) + { + Button bv; + try { + bv = new Button(m_activity, null, android.R.attr.borderlessButtonStyle); + } catch (Exception e) { + bv = new Button(m_activity); + e.printStackTrace(); + } + + bv.setText(button.m_text); + bv.setOnClickListener(button); + if (!firstButton) // first button + { + View spacer = new View(m_activity); + try { + 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) { + e.printStackTrace(); + } + } + LinearLayout.LayoutParams layout = new LinearLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, 1.0f); + buttonsLayout.addView(bv, layout); + firstButton = false; + } + + 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); + 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()); + } + 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(() -> { + if (m_dialog != null && m_dialog.isShowing()) + m_dialog.dismiss(); + reset(); + }); + } + + public long handler() + { + return m_handler; + } + + public void reset() + { + m_standardIcon = 0; + m_title = null; + m_text = null; + m_informativeText = null; + m_detailedText = null; + m_buttonsList = null; + m_dialog = null; + m_handler = 0; + } + + 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; + private AlertDialog m_dialog; + private long m_handler = 0; + private Resources.Theme m_theme; +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtNative.java b/src/android/jar/src/org/qtproject/qt/android/QtNative.java new file mode 100644 index 0000000000..b2a2887ad5 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtNative.java @@ -0,0 +1,465 @@ +// Copyright (C) 2016 BogDan Vatra <bogdan@kde.org> +// 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.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.UriPermission; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +class QtNative +{ + private static WeakReference<Activity> m_activity = null; + private static WeakReference<Service> m_service = null; + public static final Object m_mainActivityMutex = new Object(); // mutex used to synchronize runnable operations + + private static final ApplicationStateDetails m_stateDetails = new ApplicationStateDetails(); + + public 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; + + 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 + public static ClassLoader classLoader() + { + return m_classLoader; + } + + public static void setClassLoader(ClassLoader classLoader) + { + m_classLoader = classLoader; + } + + public static void setActivity(Activity qtMainActivity) + { + synchronized (m_mainActivityMutex) { + m_activity = new WeakReference<>(qtMainActivity); + } + } + + public static void setService(Service qtMainService) + { + synchronized (m_mainActivityMutex) { + m_service = new WeakReference<>(qtMainService); + } + } + + @UsedFromNativeCode + public static Activity activity() + { + synchronized (m_mainActivityMutex) { + return m_activity != null ? m_activity.get() : null; + } + } + + public static boolean isActivityValid() + { + return m_activity != null && m_activity.get() != null; + } + + @UsedFromNativeCode + public static Service service() + { + synchronized (m_mainActivityMutex) { + return m_service != null ? m_service.get() : null; + } + } + + public static boolean isServiceValid() + { + return m_service != null && m_service.get() != null; + } + + @UsedFromNativeCode + public static Context getContext() { + if (isActivityValid()) + return m_activity.get(); + return service(); + } + + @UsedFromNativeCode + public static String[] getStringArray(String joinedString) + { + return joinedString.split(","); + } + + private static String getCurrentMethodNameLog() + { + return new Exception().getStackTrace()[1].getMethodName() + ": "; + } + + /** @noinspection SameParameterValue*/ + private static Uri getUriWithValidPermission(Context context, String uri, String openMode) + { + Uri parsedUri; + try { + parsedUri = Uri.parse(uri); + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + + try { + String scheme = parsedUri.getScheme(); + + // We only want to check permissions for content Uris + if (scheme != null && scheme.compareTo("content") != 0) + return parsedUri; + + List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions(); + String uriStr = parsedUri.getPath(); + + for (int i = 0; i < permissions.size(); ++i) { + Uri iterUri = permissions.get(i).getUri(); + boolean isRequestPermission = permissions.get(i).isReadPermission(); + + if (!openMode.equals("r")) + isRequestPermission = permissions.get(i).isWritePermission(); + + if (Objects.equals(iterUri.getPath(), uriStr) && isRequestPermission) + return iterUri; + } + + // if we only have transient permissions on uri all the above will fail, + // but we will be able to read the file anyway, so continue with uri here anyway + // and check for SecurityExceptions later + return parsedUri; + } catch (SecurityException e) { + Log.e(QtTAG, getCurrentMethodNameLog() + e); + return parsedUri; + } + } + + @UsedFromNativeCode + public static boolean openURL(Context context, String url, String mime) + { + final Uri uri = getUriWithValidPermission(context, url, "r"); + if (uri == null) { + Log.e(QtTAG, getCurrentMethodNameLog() + "received invalid/null Uri"); + return false; + } + + try { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (!mime.isEmpty()) + intent.setDataAndType(uri, mime); + + 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); + return false; + } + } + + static QtThread getQtThread() { + return m_qtThread; + } + + interface AppStateDetailsListener { + default void onAppStateDetailsChanged(ApplicationStateDetails details) {} + default void onNativePluginIntegrationReadyChanged(boolean ready) {} + } + + // Keep in sync with src/corelib/global/qnamespace.h + public static class ApplicationState { + static final int ApplicationSuspended = 0x0; + static final int ApplicationHidden = 0x1; + static final int ApplicationInactive = 0x2; + static final int ApplicationActive = 0x4; + } + + public static class ApplicationStateDetails { + int state = ApplicationState.ApplicationSuspended; + boolean nativePluginIntegrationReady = false; + boolean isStarted = false; + } + + public static ApplicationStateDetails getStateDetails() + { + return m_stateDetails; + } + + public static void setStarted(boolean started) + { + m_stateDetails.isStarted = started; + notifyAppStateDetailsChanged(m_stateDetails); + } + + @UsedFromNativeCode + public static void notifyNativePluginIntegrationReady(boolean ready) + { + m_stateDetails.nativePluginIntegrationReady = ready; + notifyNativePluginIntegrationReadyChanged(ready); + notifyAppStateDetailsChanged(m_stateDetails); + } + + public static void setApplicationState(int state) + { + synchronized (m_mainActivityMutex) { + 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); + } + } + + 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 + public static void runAction(Runnable action) + { + runAction(action, true); + } + + public static void runAction(Runnable action, boolean queueWhenInactive) + { + synchronized (m_mainActivityMutex) { + final Looper mainLooper = Looper.getMainLooper(); + final Handler handler = new Handler(mainLooper); + + 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 (isActivityValid()) { + if (m_stateDetails.state == ApplicationState.ApplicationActive) + m_activity.get().runOnUiThread(runPendingCppRunnablesRunnable); + else + runAction(runPendingCppRunnablesRunnable); + } else { + final Looper mainLooper = Looper.getMainLooper(); + final Thread looperThread = mainLooper.getThread(); + if (looperThread.equals(Thread.currentThread())) { + runPendingCppRunnablesRunnable.run(); + } else { + final Handler handler = new Handler(mainLooper); + handler.post(runPendingCppRunnablesRunnable); + } + } + } + } + + @UsedFromNativeCode + private static void setViewVisibility(final View view, final boolean visible) + { + runAction(() -> view.setVisibility(visible ? View.VISIBLE : View.GONE)); + } + + public static void startApplication(String params, String mainLib) + { + synchronized (m_mainActivityMutex) { + 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_stateDetails.isStarted = true; + notifyAppStateDetailsChanged(m_stateDetails); + } + } + + public static void quitApp() + { + runAction(() -> { + quitQtAndroidPlugin(); + if (isActivityValid()) + m_activity.get().finish(); + if (isServiceValid()) + m_service.get().stopSelf(); + m_stateDetails.isStarted = false; + notifyAppStateDetailsChanged(m_stateDetails); + }); + } + + @UsedFromNativeCode + public static int checkSelfPermission(String permission) + { + synchronized (m_mainActivityMutex) { + Context context = getContext(); + PackageManager pm = context.getPackageManager(); + return pm.checkPermission(permission, context.getPackageName()); + } + } + + @UsedFromNativeCode + private static byte[][] getSSLCertificates() + { + ArrayList<byte[]> certificateList = new ArrayList<>(); + + try { + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init((KeyStore) null); + + for (TrustManager manager : factory.getTrustManagers()) { + if (manager instanceof X509TrustManager) { + X509TrustManager trustManager = (X509TrustManager) manager; + + for (X509Certificate certificate : trustManager.getAcceptedIssuers()) { + byte[] buffer = certificate.getEncoded(); + certificateList.add(buffer); + } + } + } + } catch (Exception e) { + Log.e(QtTAG, "Failed to get certificates", e); + } + + byte[][] certificateArray = new byte[certificateList.size()][]; + certificateArray = certificateList.toArray(certificateArray); + return certificateArray; + } + + @UsedFromNativeCode + private static String[] listAssetContent(android.content.res.AssetManager asset, String path) { + String [] list; + ArrayList<String> res = new ArrayList<>(); + try { + list = asset.list(path); + if (list != null) { + for (String file : list) { + try { + String[] isDir = asset.list(path.length() > 0 ? path + "/" + file : file); + if (isDir != null && isDir.length > 0) + file += "/"; + res.add(file); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return res.toArray(new String[0]); + } + + // 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 + + // surface methods + public static native void setSurface(int id, Object surface); + // surface methods + + // window methods + public static native void updateWindow(); + // window methods + + // application methods + public 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); + // menu methods + + // activity methods + public static native void onActivityResult(int requestCode, int resultCode, Intent data); + public static native void onNewIntent(Intent data); + + public static native void runPendingCppRunnables(); + + public static native void sendRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); + // activity methods + + // service methods + public static native IBinder onBind(Intent intent); + // service methods +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtNativeAccessibility.java b/src/android/jar/src/org/qtproject/qt/android/QtNativeAccessibility.java new file mode 100644 index 0000000000..dd2cead8cd --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtNativeAccessibility.java @@ -0,0 +1,22 @@ +// 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.graphics.Rect; +import android.view.accessibility.AccessibilityNodeInfo; + +class QtNativeAccessibility +{ + static native void setActive(boolean enable); + static native int[] childIdListForAccessibleObject(int objectId); + static native int parentId(int objectId); + static native String descriptionForAccessibleObject(int objectId); + static native Rect screenRect(int objectId); + static native int hitTest(float x, float y); + static native boolean clickAction(int objectId); + static native boolean scrollForward(int objectId); + static native boolean scrollBackward(int objectId); + + static native boolean populateNode(int objectId, AccessibilityNodeInfo node); +} 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..3dae587a71 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtRootLayout.java @@ -0,0 +1,98 @@ +// 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. +*/ +public class QtRootLayout extends QtLayout +{ + private int m_activityDisplayRotation = -1; + private int m_ownDisplayRotation = -1; + private int m_nativeOrientation = -1; + private int m_previousRotation = -1; + + public QtRootLayout(Context context) + { + super(context); + } + + public void setActivityDisplayRotation(int rotation) + { + m_activityDisplayRotation = rotation; + } + + public void setNativeOrientation(int orientation) + { + m_nativeOrientation = orientation; + } + + public int displayRotation() + { + return m_ownDisplayRotation; + } + + @Override + protected void onSizeChanged (int w, int h, int oldw, int oldh) + { + Activity activity = (Activity)getContext(); + if (activity == null) + return; + + DisplayMetrics realMetrics = new DisplayMetrics(); + Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + ? activity.getWindowManager().getDefaultDisplay() + : activity.getDisplay(); + + if (display == null) + return; + + display.getRealMetrics(realMetrics); + if ((realMetrics.widthPixels > realMetrics.heightPixels) != (w > h)) { + // This is an intermediate state during display rotation. + // The new size is still reported for old orientation, while + // realMetrics contain sizes for new orientation. Setting + // such parameters will produce inconsistent results, so + // we just skip them. + // We will have another onSizeChanged() with normal values + // a bit later. + 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/QtServiceEmbeddedDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceEmbeddedDelegate.java new file mode 100644 index 0000000000..29f1d1790f --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceEmbeddedDelegate.java @@ -0,0 +1,115 @@ +// 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); + } + + @UsedFromNativeCode + QtInputDelegate getInputDelegate() + { + // TODO Implement text input (QTBUG-122552) + return null; + } + + @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); + + 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 new file mode 100644 index 0000000000..3165de4811 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtSurface.java @@ -0,0 +1,53 @@ +// 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.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PixelFormat; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +@SuppressLint("ViewConstructor") +class QtSurface extends SurfaceView implements SurfaceHolder.Callback +{ + private QtSurfaceInterface m_surfaceCallback; + + 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); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) + { + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) + { + if (width < 1 || height < 1) + return; + if (m_surfaceCallback != null) + m_surfaceCallback.onSurfaceChanged(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) + { + 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..8df442f730 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtSurfaceInterface.java @@ -0,0 +1,13 @@ +// 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; + + +public 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..828838a9f0 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtTextureView.java @@ -0,0 +1,52 @@ +// 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.PixelFormat; +import android.graphics.SurfaceTexture; +import android.util.Log; +import android.view.Surface; +import android.view.TextureView; + +public class QtTextureView extends TextureView implements TextureView.SurfaceTextureListener +{ + private 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 new file mode 100644 index 0000000000..0943ad3265 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtThread.java @@ -0,0 +1,81 @@ +// Copyright (C) 2018 BogDan Vatra <bogdan@kdab.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 java.util.ArrayList; +import java.util.concurrent.Semaphore; + +class QtThread { + private final ArrayList<Runnable> m_pendingRunnables = new ArrayList<>(); + private boolean m_exit = false; + private final Thread m_qtThread = new Thread(new Runnable() { + @Override + public void run() { + while (!m_exit) { + try { + ArrayList<Runnable> pendingRunnables; + synchronized (m_qtThread) { + if (m_pendingRunnables.size() == 0) + m_qtThread.wait(); + pendingRunnables = new ArrayList<>(m_pendingRunnables); + m_pendingRunnables.clear(); + } + for (Runnable runnable : pendingRunnables) + runnable.run(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + QtThread() { + m_qtThread.setName("qtMainLoopThread"); + m_qtThread.start(); + } + + public void post(final Runnable runnable) { + synchronized (m_qtThread) { + m_pendingRunnables.add(runnable); + m_qtThread.notify(); + } + } + + 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(() -> { + runnable.run(); + sem.release(); + }); + m_qtThread.notify(); + } + try { + sem.acquire(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void exit() + { + m_exit = true; + synchronized (m_qtThread) { + m_qtThread.notify(); + } + try { + m_qtThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} 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..ddf70b3b5b --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtView.java @@ -0,0 +1,190 @@ +// 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 QtView for embedding a QWindow. Instantiating a QtView will load the Qt libraries + * if they have not already been loaded, including the app library specified by appName, and + * starting the said Qt app. + * @param context the hosting Context + * @param appLibName the name of the Qt app library to load and start. This corresponds to the + target name set in Qt app's CMakeLists.txt + **/ + public QtView(Context context, String appLibName) throws InvalidParameterException { + super(context); + if (appLibName == null || appLibName.isEmpty()) { + throw new InvalidParameterException("QtView: argument 'appLibName' may not be empty "+ + "or null"); + } + + QtEmbeddedLoader loader = new QtEmbeddedLoader(context); + m_viewInterface = QtEmbeddedDelegateFactory.create((Activity)context); + loader.setMainLibraryName(appLibName); + 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); + } + } + } + }); + loader.loadQtLibraries(); + // Start Native Qt application + m_viewInterface.startQtApplication(loader.getApplicationParameters(), + loader.getMainLibraryPath()); + } + + @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 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..d72e69d32a --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtWindow.java @@ -0,0 +1,215 @@ +// 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.util.Log; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; + +class QtWindow extends QtLayout implements QtSurfaceInterface { + private final static String TAG = "QtWindow"; + + private View m_surfaceContainer; + private View m_nativeView; + private HashMap<Integer, QtWindow> m_childWindows = new HashMap<Integer, QtWindow>(); + 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, QtInputDelegate delegate) + { + super(context); + setId(View.generateViewId()); + m_editText = new QtEditText(context, delegate); + 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() { + public void onLongPress(MotionEvent event) { + QtInputDelegate.longPress(getId(), (int) event.getX(), (int) event.getY()); + } + }); + m_gestureDetector.setIsLongpressEnabled(true); + }); + } + + 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()); + } + + public void removeWindow() + { + if (m_parentWindow != null) + m_parentWindow.removeChildWindow(getId()); + } + + 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); + }); + } + + public void destroySurface() + { + QtNative.runAction(()-> { + if (m_surfaceContainer != null) { + removeView(m_surfaceContainer); + m_surfaceContainer = null; + } + }, false); + } + + 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)); + }); + } + + 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); + }); + } + + public void bringChildToFront(int id) + { + QtNative.runAction(()-> { + View view = m_childWindows.get(id); + if (view != null) { + if (getChildCount() > 0) + moveChild(view, getChildCount() - 1); + } + }); + } + + public void bringChildToBack(int id) { + QtNative.runAction(()-> { + View view = m_childWindows.get(id); + if (view != null) { + moveChild(view, 0); + } + }); + } + + 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); + } + + QtWindow parent() + { + return m_parentWindow; + } +} 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 new file mode 100644 index 0000000000..bd837570fe --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidBinder.java @@ -0,0 +1,36 @@ +// Copyright (C) 2017 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.extras; + +import android.os.Binder; +import android.os.Parcel; + +import org.qtproject.qt.android.UsedFromNativeCode; + +class QtAndroidBinder extends Binder +{ + @UsedFromNativeCode + public QtAndroidBinder(long id) + { + m_id = id; + } + + public void setId(long id) + { + synchronized(this) + { + m_id = id; + } + } + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + { + synchronized(this) + { + return QtNative.onTransact(m_id, code, data, reply, flags); + } + } + + private long m_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 new file mode 100644 index 0000000000..b70b64e3ac --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtAndroidServiceConnection.java @@ -0,0 +1,45 @@ +// Copyright (C) 2017 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.extras; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.IBinder; + +import org.qtproject.qt.android.UsedFromNativeCode; + +class QtAndroidServiceConnection implements ServiceConnection +{ + @UsedFromNativeCode + public QtAndroidServiceConnection(long id) + { + m_id = id; + } + + public void setId(long id) + { + synchronized(this) + { + m_id = id; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + synchronized(this) { + QtNative.onServiceConnected(m_id, name.flattenToString(), service); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) + { + synchronized(this) { + QtNative.onServiceDisconnected(m_id, name.flattenToString()); + } + } + + private long m_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 new file mode 100644 index 0000000000..f7ba8dd9b4 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/extras/QtNative.java @@ -0,0 +1,17 @@ +// Copyright (C) 2017 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.extras; + +import android.os.IBinder; +import android.os.Parcel; + +class QtNative { + // Binder + public static native boolean onTransact(long id, int code, Parcel data, Parcel reply, int flags); + + + // ServiceConnection + public static native void onServiceConnected(long id, String name, IBinder service); + public static native void onServiceDisconnected(long id, String name); +} |