diff options
10 files changed, 702 insertions, 821 deletions
diff --git a/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java index 672a2e28d3..67e93a6e29 100644 --- a/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java +++ b/src/android/jar/src/org/qtproject/qt/android/ExtractStyle.java @@ -5,8 +5,10 @@ package org.qtproject.qt.android; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -136,26 +138,50 @@ public class ExtractStyle { Context m_context; private final HashMap<String, DrawableCache> m_drawableCache = new HashMap<>(); - private static final String EXTRACT_STYLE_KEY = "extract.android.style"; - private static final String EXTRACT_STYLE_MINIMAL_KEY = "extract.android.style.option"; - private static boolean m_missingNormalStyle = false; private static boolean m_missingDarkStyle = false; private static String m_stylePath = null; private static boolean m_extractMinimal = false; - public static void setup(Bundle loaderParams) { - if (loaderParams.containsKey(EXTRACT_STYLE_KEY)) { - m_stylePath = loaderParams.getString(EXTRACT_STYLE_KEY); + private static final String QtTAG = "QtExtractStyle"; + + private static boolean isUiModeDark(Configuration config) + { + return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + public static String setup(Activity activity, String extractOption, int dpi) { - boolean darkModeFileMissing = !(new File(m_stylePath + "darkUiMode/style.json").exists()); - m_missingDarkStyle = Build.VERSION.SDK_INT > 28 && darkModeFileMissing; + String dataDir = activity.getApplicationInfo().dataDir; + m_stylePath = dataDir + "/qt-reserved-files/android-style/" + dpi + "/"; - m_missingNormalStyle = !(new File(m_stylePath + "style.json").exists()); + if (!extractOption.equals("default") && !extractOption.equals("full") + && !extractOption.equals("minimal") && !extractOption.equals("none")) { + Log.e(QtTAG, "Invalid extract_android_style option \"" + extractOption + + "\", defaulting to \"default\""); + extractOption = "default"; + } - m_extractMinimal = loaderParams.containsKey(EXTRACT_STYLE_MINIMAL_KEY) && - loaderParams.getBoolean(EXTRACT_STYLE_MINIMAL_KEY); + // QTBUG-69810: The extraction code will trigger compatibility warnings on Android + // SDK version >= 28 when the target SDK version is set to something lower then 28, + // so default to "none" and issue a warning if that is the case. + if (extractOption.equals("default")) { + int targetSdk = activity.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(activity, isUiModeDark(activity.getResources().getConfiguration())); + + return m_stylePath; } public static void runIfNeeded(Context context, boolean extractDarkMode) { diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java index 77d772da45..e067cf5de3 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityBase.java @@ -10,15 +10,16 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Browser; -import android.text.method.MetaKeyKeyListener; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.Window; + +import java.lang.reflect.Field; public class QtActivityBase extends Activity { @@ -48,17 +49,7 @@ public class QtActivityBase extends Activity public String QT_ANDROID_DEFAULT_THEME = null; // sets the default theme. - private final QtActivityLoader m_loader = new QtActivityLoader(this); - - private final QtActivityDelegate m_delegate = new QtActivityDelegate(); - - protected void onCreateHook(Bundle savedInstanceState) { - m_loader.APPLICATION_PARAMETERS = APPLICATION_PARAMETERS; - m_loader.ENVIRONMENT_VARIABLES = ENVIRONMENT_VARIABLES; - m_loader.QT_ANDROID_THEMES = QT_ANDROID_THEMES; - m_loader.QT_ANDROID_DEFAULT_THEME = QT_ANDROID_DEFAULT_THEME; - m_loader.onCreate(savedInstanceState); - } + private QtActivityDelegate m_delegate; public static final String EXTRA_SOURCE_INFO = "org.qtproject.qt.android.sourceInfo"; @@ -83,12 +74,58 @@ public class QtActivityBase extends Activity intent.putExtra(EXTRA_SOURCE_INFO, sourceInformation); } + private void handleActivityRestart() { + if (QtNative.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); + } + } + } + + void configureActivityTheme() { + if (QT_ANDROID_THEMES == null || QT_ANDROID_DEFAULT_THEME == null) { + if (Build.VERSION.SDK_INT < 29) { + QT_ANDROID_THEMES = new String[]{"Theme_Holo_Light"}; + QT_ANDROID_DEFAULT_THEME = "Theme_Holo_Light"; + } else { + QT_ANDROID_THEMES = new String[]{"Theme_DeviceDefault_DayNight"}; + QT_ANDROID_DEFAULT_THEME = "Theme_DeviceDefault_DayNight"; + } + } + try { + Field f = Class.forName("android.R$style").getDeclaredField(QT_ANDROID_DEFAULT_THEME); + int themeId = f.getInt(null); + setTheme(themeId); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - onCreateHook(savedInstanceState); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + + m_delegate = new QtActivityDelegate(this); + + handleActivityRestart(); addReferrer(getIntent()); + configureActivityTheme(); + + QtActivityLoader loader = new QtActivityLoader(this); + loader.setApplicationParameters(APPLICATION_PARAMETERS); + loader.setEnvironmentVariables(ENVIRONMENT_VARIABLES); + loader.setEnvironmentVariable("QT_ANDROID_THEME", QT_ANDROID_DEFAULT_THEME); + + loader.loadQtLibraries(); + m_delegate.startNativeApplication(loader.getApplicationParameters(), + loader.getMainLibrary()); } @Override diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java index e7acb06ca6..ad4931a1fc 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityDelegate.java @@ -8,15 +8,12 @@ package org.qtproject.qt.android; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.Rect; import android.os.Build; -import android.os.Bundle; -import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -39,13 +36,10 @@ import android.widget.ImageView; import android.widget.PopupMenu; import android.hardware.display.DisplayManager; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; -import java.util.Objects; import org.qtproject.qt.android.accessibility.QtAccessibilityDelegate; -import static org.qtproject.qt.android.QtConstants.*; public class QtActivityDelegate { @@ -57,13 +51,9 @@ public class QtActivityDelegate public static final int SYSTEM_UI_VISIBILITY_TRANSLUCENT = 2; private int m_systemUiVisibility = SYSTEM_UI_VISIBILITY_NORMAL; - private static String m_applicationParameters = null; - private int m_currentRotation = -1; // undefined private int m_nativeOrientation = Configuration.ORIENTATION_UNDEFINED; - private String m_mainLib; - private boolean m_started = false; private boolean m_quitApp = true; private boolean m_isPluginRunning = false; @@ -88,10 +78,25 @@ public class QtActivityDelegate }; private final QtInputDelegate m_inputDelegate = new QtInputDelegate(m_keyboardVisibilityListener); - QtActivityDelegate() { } - - QtInputDelegate getInputDelegate() + QtActivityDelegate(Activity activity) { + m_activity = activity; + QtNative.setActivity(m_activity, this); + + setActionBarVisibility(false); + + try { + m_inputDelegate.setSoftInputMode(m_activity.getPackageManager() + .getActivityInfo(m_activity.getComponentName(), 0).softInputMode); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + DisplayManager displayManager = (DisplayManager) m_activity.getSystemService(Context.DISPLAY_SERVICE); + displayManager.registerDisplayListener(m_displayListener, null); + } + + QtInputDelegate getInputDelegate() { return m_inputDelegate; } @@ -194,24 +199,7 @@ public class QtActivityDelegate return m_contextMenuVisible; } - int getAppIconSize(Activity a) - { - int size = a.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); - if (size < 36 || size > 512) { // check size sanity - DisplayMetrics metrics = new DisplayMetrics(); - a.getWindowManager().getDefaultDisplay().getMetrics(metrics); - size = metrics.densityDpi / 10 * 3; - if (size < 36) - size = 36; - - if (size > 512) - size = 512; - } - - return size; - } - - private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() + private final DisplayManager.DisplayListener m_displayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { @@ -252,14 +240,14 @@ public class QtActivityDelegate } }; - public boolean updateActivity(Activity activity) - { + public boolean updateActivityAfterRestart(Activity activity) { try { // set new activity - loadActivity(activity); + m_activity = activity; + QtNative.setActivity(m_activity, this); // update the new activity content view to old layout - ViewGroup layoutParent = (ViewGroup)m_layout.getParent(); + ViewGroup layoutParent = (ViewGroup) m_layout.getParent(); if (layoutParent != null) layoutParent.removeView(m_layout); @@ -274,169 +262,37 @@ public class QtActivityDelegate } } - private void loadActivity(Activity activity) - throws NoSuchMethodException, PackageManager.NameNotFoundException - { - m_activity = activity; - - QtNative.setActivity(m_activity, this); - setActionBarVisibility(false); - - m_inputDelegate.setSoftInputMode(m_activity.getPackageManager().getActivityInfo(m_activity.getComponentName(), 0).softInputMode); - - DisplayManager displayManager = (DisplayManager)m_activity.getSystemService(Context.DISPLAY_SERVICE); - displayManager.registerDisplayListener(displayListener, null); + public void onTerminate() { + QtNative.terminateQt(); + QtNative.m_qtThread.exit(); } - public boolean loadApplication(Activity activity, ClassLoader classLoader, Bundle loaderParams) + public void startNativeApplication(ArrayList<String> appParams, String mainLib) { - /// check parameters integrity - if (!loaderParams.containsKey(NATIVE_LIBRARIES_KEY) - || !loaderParams.containsKey(BUNDLED_LIBRARIES_KEY) - || !loaderParams.containsKey(ENVIRONMENT_VARIABLES_KEY)) { - return false; - } - - try { - loadActivity(activity); - QtNative.setClassLoader(classLoader); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - - if (loaderParams.containsKey(STATIC_INIT_CLASSES_KEY)) { - for (String className: Objects.requireNonNull(loaderParams.getStringArray(STATIC_INIT_CLASSES_KEY))) { - if (className.length() == 0) - continue; + if (m_surfaces != null) + return; + Runnable startApplication = new Runnable() { + @Override + public void run() { try { - Class<?> initClass = classLoader.loadClass(className); - Object staticInitDataObject = initClass.newInstance(); // create an instance - try { - Method m = initClass.getMethod("setActivity", Activity.class, Object.class); - m.invoke(staticInitDataObject, m_activity, this); - } catch (Exception e) { - Log.d(QtNative.QtTAG, "Class " + className + " does not implement setActivity method"); - } - - // For modules that don't need/have setActivity - try { - Method m = initClass.getMethod("setContext", Context.class); - m.invoke(staticInitDataObject, (Context)m_activity); - } catch (Exception e) { - e.printStackTrace(); - } + QtNative.startApplication(appParams, mainLib); + m_started = true; } catch (Exception e) { e.printStackTrace(); + m_activity.finish(); } } - } - QtNative.loadQtLibraries(loaderParams.getStringArrayList(NATIVE_LIBRARIES_KEY)); - ArrayList<String> libraries = loaderParams.getStringArrayList(BUNDLED_LIBRARIES_KEY); - String nativeLibsDir = QtNativeLibrariesDir.nativeLibrariesDir(m_activity); - QtNative.loadBundledLibraries(libraries, nativeLibsDir); - m_mainLib = loaderParams.getString(MAIN_LIBRARY_KEY); - // older apps provide the main library as the last bundled library; look for this if the main library isn't provided - if (null == m_mainLib && libraries.size() > 0) { - m_mainLib = libraries.get(libraries.size() - 1); - libraries.remove(libraries.size() - 1); - } - - ExtractStyle.setup(loaderParams); - ExtractStyle.runIfNeeded(m_activity, isUiModeDark(m_activity.getResources().getConfiguration())); - - QtNative.setEnvironmentVariables(loaderParams.getString(ENVIRONMENT_VARIABLES_KEY)); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", - "Droid Sans Mono;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif"); - QtNative.setEnvironmentVariable("HOME", m_activity.getFilesDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("TMPDIR", m_activity.getCacheDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS", - "Roboto;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_APP_ICON_SIZE", - String.valueOf(getAppIconSize(activity))); - - if (loaderParams.containsKey(APPLICATION_PARAMETERS_KEY)) - m_applicationParameters = loaderParams.getString(APPLICATION_PARAMETERS_KEY); - else - m_applicationParameters = ""; - - m_mainLib = QtNative.loadMainLibrary(m_mainLib, nativeLibsDir); - return m_mainLib != null; - } - - public boolean startApplication() - { - // start application - try { - - Bundle extras = m_activity.getIntent().getExtras(); - if (extras != null) { - try { - final boolean isDebuggable = (m_activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; - if (!isDebuggable) - throw new Exception(); - - if (extras.containsKey("extraenvvars")) { - try { - QtNative.setEnvironmentVariables(new String( - Base64.decode(extras.getString("extraenvvars"), Base64.DEFAULT), - "UTF-8")); - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (extras.containsKey("extraappparams")) { - try { - m_applicationParameters += "\t" + new String(Base64.decode(extras.getString("extraappparams"), Base64.DEFAULT), "UTF-8"); - } catch (Exception e) { - e.printStackTrace(); - } - } - } catch (Exception e) { - Log.e(QtNative.QtTAG, "Not in debug mode! It is not allowed to use " + - "extra arguments in non-debug mode."); - // This is not an error, so keep it silent - // e.printStackTrace(); - } - } // extras != null + }; - if (null == m_surfaces) - onCreate(null); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } + initMembers(startApplication); } - - public void onTerminate() - { - QtNative.terminateQt(); - QtNative.m_qtThread.exit(); - } - - public void onCreate(Bundle savedInstanceState) + + private void initMembers(Runnable startApplicationRunnable) { m_quitApp = true; - Runnable startApplication = null; - if (null == savedInstanceState) { - startApplication = new Runnable() { - @Override - public void run() { - try { - QtNative.startApplication(m_applicationParameters, m_mainLib); - m_started = true; - } catch (Exception e) { - e.printStackTrace(); - m_activity.finish(); - } - } - }; - } - m_layout = new QtLayout(m_activity, startApplication); + + m_layout = new QtLayout(m_activity, startApplicationRunnable); int orientation = m_activity.getResources().getConfiguration().orientation; @@ -593,11 +449,6 @@ public class QtActivityDelegate m_accessibilityDelegate = new QtAccessibilityDelegate(m_activity, m_layout, this); } - boolean isUiModeDark(Configuration config) - { - return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - } - void handleUiModeChange(int uiMode) { // QTBUG-108365 diff --git a/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java index d9cfa2f79d..99a1d49d83 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtActivityLoader.java @@ -4,98 +4,156 @@ package org.qtproject.qt.android; +import android.annotation.SuppressLint; import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Bundle; -import android.view.Window; +import android.util.Base64; +import android.util.DisplayMetrics; +import android.util.Log; -import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; public class QtActivityLoader extends QtLoader { - Activity m_activity; + private final Activity m_activity; public QtActivityLoader(Activity activity) { super(activity); m_activity = activity; - } - @Override - protected void finish() { - m_activity.finish(); + extractContextMetaData(); } @Override - protected String getTitle() { - return (String) m_activity.getTitle(); + protected void initContextInfo() { + try { + m_contextInfo = m_context.getPackageManager().getActivityInfo( + ((Activity)m_context).getComponentName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + finish(); + } } - @Override - protected void runOnUiThread(Runnable run) { - m_activity.runOnUiThread(run); + private void showErrorDialog() { + 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), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }); + errorDialog.show(); } @Override - Intent getIntent() { - return m_activity.getIntent(); + protected void finish() { + showErrorDialog(); + m_activity.finish(); } - public void onCreate(Bundle savedInstanceState) { + @Override + protected void initStaticClassesImpl(Class<?> initClass, Object staticInitDataObject) { try { - m_contextInfo = m_activity.getPackageManager().getActivityInfo( - m_activity.getComponentName(), PackageManager.GET_META_DATA); - int theme = ((ActivityInfo)m_contextInfo).getThemeResource(); - for (Field f : Class.forName("android.R$style").getDeclaredFields()) { - if (f.getInt(null) == theme) { - QT_ANDROID_THEMES = new String[] {f.getName()}; - QT_ANDROID_DEFAULT_THEME = f.getName(); - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - finish(); - return; + Method m = initClass.getMethod("setActivity", Activity.class, Object.class); + m.invoke(staticInitDataObject, m_activity, this); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + Log.d(QtTAG, "Class " + initClass.getName() + " does not implement setActivity method"); } + } - try { - m_activity.setTheme(Class.forName("android.R$style").getDeclaredField( - QT_ANDROID_DEFAULT_THEME).getInt(null)); - } catch (Exception e) { - e.printStackTrace(); + 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; } - m_activity.requestWindowFeature(Window.FEATURE_ACTION_BAR); - if (QtNative.isStarted()) { - boolean updated = QtNative.activityDelegate().updateActivity(m_activity); - if (!updated) { - // could not update the activity so restart the application - Intent intent = Intent.makeRestartActivityTask(m_activity.getComponentName()); - m_activity.startActivity(intent); - QtNative.quitApp(); - Runtime.getRuntime().exit(0); - } + return size; + } + + private void setupStyleExtraction() + { + int displayDensity = m_activity.getResources().getDisplayMetrics().densityDpi; + setEnvironmentVariable("QT_ANDROID_THEME_DISPLAY_DPI", String.valueOf(displayDensity)); - ((QtActivityBase)m_activity).getActivityDelegate().onCreate(savedInstanceState); + 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_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; } - m_displayDensity = m_activity.getResources().getDisplayMetrics().densityDpi; - - ENVIRONMENT_VARIABLES += "\tQT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME - + "/\tQT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + "\t"; + String intentArgs = intent.getStringExtra("applicationArguments"); + if (intentArgs != null) + setApplicationParameters(intentArgs); - if (m_contextInfo.metaData.containsKey("android.app.background_running") - && m_contextInfo.metaData.getBoolean("android.app.background_running")) { - ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=0\t"; - } else { - ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1\t"; + Bundle extras = intent.getExtras(); + if (extras == null) { + Log.w(QtTAG, "Null extras from the Activity's intent."); + return; } - startApp(true); + 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"); + setApplicationParameters(getDecodedUtfString(extraAppParams)); + } + } 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/QtConstants.java b/src/android/jar/src/org/qtproject/qt/android/QtConstants.java index a1026f515a..c3dfc3d9f9 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtConstants.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtConstants.java @@ -4,18 +4,6 @@ package org.qtproject.qt.android; public class QtConstants { - public static final String DEX_PATH_KEY = "dex.path"; - public static final String LIB_PATH_KEY = "lib.path"; - public static final String LOADER_CLASS_NAME_KEY = "loader.class.name"; - public static final String NATIVE_LIBRARIES_KEY = "native.libraries"; - public static final String ENVIRONMENT_VARIABLES_KEY = "environment.variables"; - public static final String APPLICATION_PARAMETERS_KEY = "application.parameters"; - public static final String BUNDLED_LIBRARIES_KEY = "bundled.libraries"; - public static final String MAIN_LIBRARY_KEY = "main.library"; - public static final String STATIC_INIT_CLASSES_KEY = "static.init.classes"; - public static final String EXTRACT_STYLE_KEY = "extract.android.style"; - public static final String EXTRACT_STYLE_MINIMAL_KEY = "extract.android.style.option"; - // Application states public static class ApplicationState { public static final int ApplicationSuspended = 0x0; diff --git a/src/android/jar/src/org/qtproject/qt/android/QtLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java index e7f260fd51..7570299e1a 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtLoader.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java @@ -4,12 +4,10 @@ package org.qtproject.qt.android; -import android.app.AlertDialog; -import android.app.Dialog; +import android.annotation.SuppressLint; import android.content.Context; import android.content.ContextWrapper; -import android.content.DialogInterface; -import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.res.Resources; import android.os.Build; @@ -17,318 +15,487 @@ import android.os.Bundle; import android.util.Log; 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.Collections; import java.util.HashMap; -import java.util.List; +import java.util.Objects; import dalvik.system.DexClassLoader; -import static org.qtproject.qt.android.QtConstants.*; - public abstract class QtLoader { - static String QtTAG = "Qt"; - - // These parameters matter in case of deploying application as system (embedded into firmware) - public static final String SYSTEM_LIB_PATH = "/system/lib/"; + protected static final String QtTAG = "QtLoader"; - public String APPLICATION_PARAMETERS = null; - public String ENVIRONMENT_VARIABLES = "QT_USE_ANDROID_NATIVE_DIALOGS=1"; - public String[] QT_ANDROID_THEMES = null; - public String QT_ANDROID_DEFAULT_THEME = null; // sets the default theme. + private final Resources m_resources; + private final String m_packageName; + private String m_preferredAbi = null; + private String m_nativeLibrariesDir = null; + private ClassLoader m_classLoader; - public ArrayList<String> m_qtLibs = null; // required qt libs - public int m_displayDensity = -1; - private ContextWrapper m_context; + protected final ContextWrapper m_context; protected ComponentInfo m_contextInfo; + protected String m_mainLib; + protected ArrayList<String> m_applicationParameters = new ArrayList<>(); + protected HashMap<String, String> m_environmentVariables = new HashMap<>(); + + /** + * 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(); } - public static void setQtTAG(String tag) - { - QtTAG = tag; + /** + * 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. + **/ + abstract protected void initContextInfo(); + + /** + * Implements the logic for finish the extended context, mostly called + * in error cases. + **/ + abstract protected void finish(); + + /** + * Context specific (Activity/Service) implementation for static classes. + * The Activity and Service loaders call different methods. + **/ + abstract protected void initStaticClassesImpl(Class<?> initClass, Object staticInitDataObject); + + /** + * 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")); + setApplicationParameters(getMetaData("android.app.arguments")); } - // Implement in subclass - protected void finish() {} + 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<>(); + } - protected String getTitle() { - return "Qt"; + for (String abi : Build.SUPPORTED_ABIS) { + if (abiLibs.containsKey(abi)) { + m_preferredAbi = abi; + return abiLibs.get(abi); + } + } + return new ArrayList<>(); } - protected void runOnUiThread(Runnable run) { - run.run(); + private void initStaticClasses() { + for (String className : getStaticInitClasses()) { + try { + Class<?> initClass = m_classLoader.loadClass(className); + Object staticInitDataObject = initClass.newInstance(); // create an instance + initStaticClassesImpl(initClass, staticInitDataObject); + + // For modules that don't need/have setActivity/setService + Method m = initClass.getMethod("setContext", Context.class); + m.invoke(staticInitDataObject, m_context); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | + NoSuchMethodException | InvocationTargetException e) { + Log.d(QtTAG, "Class " + className + " does not implement setContext method"); + } + } } - Intent getIntent() + /** + * Initialize the class loader instance and sets it via QtNative. + * This would also be used by QJniObject API. + **/ + private void initClassLoader() { - return null; + // directory where optimized DEX files should be written. + String outDexPath = m_context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath(); + m_classLoader = new DexClassLoader("", outDexPath, null, m_context.getClassLoader()); + QtNative.setClassLoader(m_classLoader); } - // Implement in subclass - private final List<String> supportedAbis = Arrays.asList(Build.SUPPORTED_ABIS); - private String preferredAbi = null; + /** + * Returns the context's main library absolute path, + * or null if the library hasn't been loaded yet. + **/ + public String getMainLibrary() { + return m_mainLib; + } + + /** + * 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 ArrayList<String> getApplicationParameters() { + return m_applicationParameters; + } - private ArrayList<String> prefferedAbiLibs(String []libs) + /** + * Adds a parameter string to the internal array list of parameters. + **/ + public void setApplicationParameter(String param) { - HashMap<String, ArrayList<String>> abisLibs = new HashMap<>(); - for (String lib : libs) { - String[] archLib = lib.split(";", 2); - if (preferredAbi != null && !archLib[0].equals(preferredAbi)) - continue; - if (!abisLibs.containsKey(archLib[0])) - abisLibs.put(archLib[0], new ArrayList<String>()); - abisLibs.get(archLib[0]).add(archLib[1]); - } + if (param == null || param.isEmpty()) + return; + m_applicationParameters.add(param); + } - if (preferredAbi != null) { - if (abisLibs.containsKey(preferredAbi)) { - return abisLibs.get(preferredAbi); - } - return new ArrayList<String>(); - } + /** + * Adds a list of parameters to the internal array list of parameters. + * This expects the parameters separated by '\t'. + **/ + public void setApplicationParameters(String params) + { + if (params == null || params.isEmpty()) + return; - for (String abi: supportedAbis) { - if (abisLibs.containsKey(abi)) { - preferredAbi = abi; - return abisLibs.get(abi); - } - } - return new ArrayList<String>(); + for (String param : params.split("\t")) + setApplicationParameter(param); } - // this function is used to load and start the loader - private void loadApplication(Bundle loaderParams) + /** + * Sets a single key/value environment variable pair. + **/ + public void setEnvironmentVariable(String key, String value) { - final Resources resources = m_context.getResources(); - final String packageName = m_context.getPackageName(); try { - // add all bundled Qt libs to loader params - int id = resources.getIdentifier("bundled_libs", "array", packageName); - final String[] bundledLibs = resources.getStringArray(id); - ArrayList<String> libs = new ArrayList<>(prefferedAbiLibs(bundledLibs)); - - String libName = null; - if (m_contextInfo.metaData.containsKey("android.app.lib_name")) { - libName = m_contextInfo.metaData.getString("android.app.lib_name") + "_" + preferredAbi; - loaderParams.putString(MAIN_LIBRARY_KEY, libName); //main library contains main() function - } - - loaderParams.putStringArrayList(BUNDLED_LIBRARIES_KEY, libs); - - // load and start QtLoader class - DexClassLoader classLoader = new DexClassLoader(loaderParams.getString(DEX_PATH_KEY), // .jar/.apk files - m_context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath(), // directory where optimized DEX files should be written. - loaderParams.containsKey(LIB_PATH_KEY) ? loaderParams.getString(LIB_PATH_KEY) : null, // libs folder (if exists) - m_context.getClassLoader()); // parent loader - - if (m_context instanceof QtActivityBase) { - QtActivityBase activityBase = (QtActivityBase)m_context; - QtActivityDelegate activityDelegate = activityBase.getActivityDelegate(); - if (!activityDelegate.loadApplication(activityBase, classLoader, loaderParams)) - throw new Exception(""); - if (!activityDelegate.startApplication()) - throw new Exception(""); - } else if (m_context instanceof QtServiceBase) { - QtServiceBase serviceBase = (QtServiceBase)m_context; - QtServiceDelegate serviceDelegate = serviceBase.getServiceDelegate(); - if (!serviceDelegate.loadApplication(serviceBase, classLoader, loaderParams)) - throw new Exception(""); - if (!serviceDelegate.startApplication()) - throw new Exception(""); - } + 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(); - AlertDialog errorDialog = new AlertDialog.Builder(m_context).create(); - 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), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - finish(); - } - }); - errorDialog.show(); } } - public void startApp(final boolean firstStart) + /** + * 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 '\t'. + * Ex: key1=val1\tkey2=val2\tkey3=val3 + **/ + public void setEnvironmentVariables(String environmentVariables) { - try { - final Resources resources = m_context.getResources(); - final String packageName = m_context.getPackageName(); - int id = resources.getIdentifier("qt_libs", "array", packageName); - m_qtLibs = prefferedAbiLibs(resources.getStringArray(id)); - - id = resources.getIdentifier("use_local_qt_libs", "string", packageName); - final int useLocalLibs = Integer.parseInt(resources.getString(id)); - - if (useLocalLibs == 1) { - ArrayList<String> libraryList = new ArrayList<>(); - String libsDir = null; - String bundledLibsDir = null; - - id = resources.getIdentifier("bundle_local_qt_libs", "string", packageName); - final int bundleLocalLibs = Integer.parseInt(resources.getString(id)); - if (bundleLocalLibs == 0) { - String systemLibsPrefix; - final String systemLibsKey = "android.app.system_libs_prefix"; - // First check if user has provided system libs prefix in AndroidManifest - if (m_contextInfo.applicationInfo.metaData != null && - m_contextInfo.applicationInfo.metaData.containsKey(systemLibsKey)) { - systemLibsPrefix = m_contextInfo.applicationInfo.metaData.getString(systemLibsKey); - } else { - // If not, check if it's provided by androiddeployqt in libs.xml - id = resources.getIdentifier("system_libs_prefix","string", - packageName); - systemLibsPrefix = resources.getString(id); - } - if (systemLibsPrefix.isEmpty()) { - systemLibsPrefix = SYSTEM_LIB_PATH; - Log.e(QtTAG, "It looks like app deployed using Unbundled " - + "deployment. It may be necessary to specify path to 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"); - Log.e(QtTAG, "Using " + SYSTEM_LIB_PATH + " as default path"); - } + if (environmentVariables == null || environmentVariables.isEmpty()) + return; - File systemLibraryDir = new File(systemLibsPrefix); - if (systemLibraryDir.exists() && systemLibraryDir.isDirectory() - && systemLibraryDir.list().length > 0) { - libsDir = systemLibsPrefix; - bundledLibsDir = systemLibsPrefix; - } else { - Log.e(QtTAG, - "System library directory " + systemLibsPrefix - + " does not exist or is empty."); - } - } else { - String nativeLibraryPrefix = m_context.getApplicationInfo().nativeLibraryDir + "/"; - File nativeLibraryDir = new File(nativeLibraryPrefix); - if (nativeLibraryDir.exists() && nativeLibraryDir.isDirectory() && nativeLibraryDir.list().length > 0) { - libsDir = nativeLibraryPrefix; - bundledLibsDir = nativeLibraryPrefix; - } else { - Log.e(QtTAG, - "Native library directory " + nativeLibraryPrefix - + " does not exist or is empty."); - } + for (String variable : environmentVariables.split("\t")) { + 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"); + } - if (libsDir == null) - throw new Exception(""); + 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 += "/"; + } - if (m_qtLibs != null) { - String libPrefix = libsDir + "lib"; - for (String lib : m_qtLibs) - libraryList.add(libPrefix + lib + ".so"); - } + /** + * Returns the application level metadata. + **/ + private String getApplicationMetaData(String key) { + if (m_contextInfo == null) + return ""; - id = resources.getIdentifier("load_local_libs", "array", packageName); - ArrayList<String> localLibs = prefferedAbiLibs(resources.getStringArray(id)); - for (String libs : localLibs) { - for (String lib : libs.split(":")) { - if (!lib.isEmpty()) - libraryList.add(libsDir + lib); - } - } - if (bundledLibsDir != null) { - ENVIRONMENT_VARIABLES += "\tQT_PLUGIN_PATH=" + bundledLibsDir; - ENVIRONMENT_VARIABLES += "\tQML_PLUGIN_PATH=" + bundledLibsDir; - } + ApplicationInfo applicationInfo = m_contextInfo.applicationInfo; + if (applicationInfo == null) + return ""; - Bundle loaderParams = new Bundle(); - loaderParams.putString(DEX_PATH_KEY, new String()); + Bundle metadata = applicationInfo.metaData; + if (metadata == null || !metadata.containsKey(key)) + return ""; - id = resources.getIdentifier("static_init_classes", "string", packageName); - loaderParams.putStringArray(STATIC_INIT_CLASSES_KEY, resources.getString(id) - .split(":")); + return metadata.getString(key); + } - loaderParams.putStringArrayList(NATIVE_LIBRARIES_KEY, libraryList); + /** + * 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 ""; - String themePath = m_context.getApplicationInfo().dataDir + "/qt-reserved-files/android-style/"; - String stylePath = themePath + m_displayDensity + "/"; + return metadata.getString(key); + } - String extractOption = "default"; - if (m_contextInfo.metaData.containsKey("android.app.extract_android_style")) { - extractOption = m_contextInfo.metaData.getString("android.app.extract_android_style"); - if (!extractOption.equals("default") && !extractOption.equals("full") && !extractOption.equals("minimal") && !extractOption.equals("none")) { - Log.e(QtTAG, "Invalid extract_android_style option \"" + extractOption + "\", defaulting to \"default\""); - extractOption = "default"; - } - } + @SuppressLint("DiscouragedApi") + private ArrayList<String> getQtLibrariesList() { + int id = m_resources.getIdentifier("qt_libs", "array", m_packageName); + return preferredAbiLibs(m_resources.getStringArray(id)); + } - // 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")) { - final int targetSdkVersion = m_context.getApplicationInfo().targetSdkVersion; - if (targetSdkVersion < 28 && Build.VERSION.SDK_INT >= 28) { - Log.e(QtTAG, "extract_android_style option set to \"none\" when targetSdkVersion is less then 28"); - extractOption = "none"; - } - } + @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; + } - if (!extractOption.equals("none")) { - loaderParams.putString(EXTRACT_STYLE_KEY, stylePath); - loaderParams.putBoolean(EXTRACT_STYLE_MINIMAL_KEY, extractOption.equals("minimal")); - } + @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; + } - if (extractOption.equals("full")) - ENVIRONMENT_VARIABLES += "\tQT_USE_ANDROID_NATIVE_STYLE=1"; + @SuppressLint("DiscouragedApi") + private String getSystemLibsPrefix() { + int id = m_resources.getIdentifier("system_libs_prefix", "string", m_packageName); + return m_resources.getString(id); + } - ENVIRONMENT_VARIABLES += "\tANDROID_STYLE_PATH=" + stylePath; + @SuppressLint("DiscouragedApi") + private ArrayList<String> getLocalLibrariesList() { + int id = m_resources.getIdentifier("load_local_libs", "array", m_packageName); + return preferredAbiLibs(m_resources.getStringArray(id)); + } - if (m_contextInfo.metaData.containsKey("android.app.trace_location")) { - String loc = m_contextInfo.metaData.getString("android.app.trace_location"); - ENVIRONMENT_VARIABLES += "\tQTRACE_LOCATION="+loc; - } + @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; + } - loaderParams.putString(ENVIRONMENT_VARIABLES_KEY, ENVIRONMENT_VARIABLES); + @SuppressLint("DiscouragedApi") + private String[] getBundledLibs() { + int id = m_resources.getIdentifier("bundled_libs", "array", m_packageName); + return m_resources.getStringArray(id); + } - String appParams = null; - if (APPLICATION_PARAMETERS != null) - appParams = APPLICATION_PARAMETERS; + /** + * 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; + } - Intent intent = getIntent(); - if (intent != null) { - String parameters = intent.getStringExtra("applicationArguments"); - if (parameters != null) - if (appParams == null) - appParams = parameters; - else - appParams += '\t' + parameters; - } + if (m_nativeLibrariesDir == null) + parseNativeLibrariesDir(); - if (m_contextInfo.metaData.containsKey("android.app.arguments")) { - String parameters = m_contextInfo.metaData.getString("android.app.arguments"); - if (appParams == null) - appParams = parameters; - else - appParams += '\t' + parameters; - } + 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); - if (appParams != null) - loaderParams.putString(APPLICATION_PARAMETERS_KEY, appParams); + // Load native Qt APK libraries + ArrayList<String> nativeLibraries = getQtLibrariesList(); + nativeLibraries.addAll(getLocalLibrariesList()); - loadApplication(loaderParams); - return; + 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; + } + + // Load main lib + if (!loadMainLibrary(getMetaData("android.app.lib_name") + "_" + 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.i(QtTAG, "Can't find '" + library + "'"); } } catch (Exception e) { - Log.e(QtTAG, "Can't create main activity", e); + Log.i(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) { + if (!libName.startsWith("lib")) + libName = "lib" + libName; + if (!libName.endsWith(".so")) + libName = libName + ".so"; + 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(new Runnable() { + @Override + public void run() { + m_mainLib = loadLibraryHelper(mainLibPath); + if (m_mainLib == 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(new Runnable() { + @Override + public void 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/QtNative.java b/src/android/jar/src/org/qtproject/qt/android/QtNative.java index dff4a27a36..f5df382b00 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtNative.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtNative.java @@ -215,102 +215,8 @@ public class QtNative } } - // this method loads full path libs - public static void loadQtLibraries(final ArrayList<String> libraries) - { - m_qtThread.run(new Runnable() { - @Override - public void run() { - if (libraries == null) - return; - for (String libName : libraries) { - try { - File f = new File(libName); - if (f.exists()) - System.load(libName); - else - Log.i(QtTAG, "Can't find '" + libName + "'"); - } catch (SecurityException e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } catch (Exception e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } - } - } - }); - } - - // this method loads bundled libs by name. - public static void loadBundledLibraries(final ArrayList<String> libraries, final String nativeLibraryDir) - { - m_qtThread.run(new Runnable() { - @Override - public void run() { - if (libraries == null) - return; - - for (String libName : libraries) { - try { - String libNameTemplate = "lib" + libName + ".so"; - File f = new File(nativeLibraryDir + libNameTemplate); - if (!f.exists()) { - Log.i(QtTAG, "Can't find '" + f.getAbsolutePath()); - try { - ApplicationInfo info = getContext().getApplicationContext().getPackageManager() - .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir; - if (info.metaData.containsKey("android.app.system_libs_prefix")) - systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix"); - f = new File(systemLibraryDir + libNameTemplate); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (f.exists()) - System.load(f.getAbsolutePath()); - else - Log.i(QtTAG, "Can't find '" + f.getAbsolutePath()); - } catch (Exception e) { - Log.i(QtTAG, "Can't load '" + libName + "'", e); - } - } - } - }); - } - - public static String loadMainLibrary(final String mainLibrary, final String nativeLibraryDir) - { - final String[] res = new String[1]; - res[0] = null; - m_qtThread.run(new Runnable() { - @Override - public void run() { - try { - String mainLibNameTemplate = "lib" + mainLibrary + ".so"; - File f = new File(nativeLibraryDir + mainLibNameTemplate); - if (!f.exists()) { - try { - ApplicationInfo info = getContext().getApplicationContext().getPackageManager() - .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir; - if (info.metaData.containsKey("android.app.system_libs_prefix")) - systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix"); - f = new File(systemLibraryDir + mainLibNameTemplate); - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - if (!f.exists()) - return; - System.load(f.getAbsolutePath()); - res[0] = f.getAbsolutePath(); - } catch (Exception e) { - Log.e(QtTAG, "Can't load '" + mainLibrary + "'", e); - } - } - }); - return res[0]; + static QtThread getQtThread() { + return m_qtThread; } public static void setActivity(Activity qtMainActivity, QtActivityDelegate qtActivityDelegate) @@ -430,17 +336,12 @@ public class QtNative return new Size(bounds.width(), bounds.height()); } - public static boolean startApplication(String params, String mainLib) throws Exception + public static boolean startApplication(ArrayList<String> params, String mainLib) { - if (params == null) - params = "-platform\tandroid"; - final boolean[] res = new boolean[1]; - res[0] = false; synchronized (m_mainActivityMutex) { - if (params.length() > 0 && !params.startsWith("\t")) - params = "\t" + params; - final String qtParams = mainLib + params; + String paramsStr = String.join("\t", params); + final String qtParams = mainLib + "\t" + paramsStr; m_qtThread.run(new Runnable() { @Override public void run() { @@ -972,40 +873,6 @@ public class QtNative return res.toArray(new String[res.size()]); } - /** - *Sets a single environment variable - * - * returns true if the value was set, false otherwise. - * in case it cannot set value will log the exception - **/ - public static void setEnvironmentVariable(String key, String value) - { - try { - android.system.Os.setenv(key, value, true); - } catch (Exception e) { - Log.e(QtNative.QtTAG, "Could not set environment variable:" + key + "=" + value); - e.printStackTrace(); - } - } - - /** - *Sets multiple environment variables - * - * Uses '\t' as divider between variables and '=' between key/value - * Ex: key1=val1\tkey2=val2\tkey3=val3 - * Note: it assumed that the key cannot have '=' but the value can - **/ - public static void setEnvironmentVariables(String environmentVariables) - { - for (String variable : environmentVariables.split("\t")) { - String[] keyvalue = variable.split("=", 2); - if (keyvalue.length < 2 || keyvalue[0].isEmpty()) - continue; - - setEnvironmentVariable(keyvalue[0], keyvalue[1]); - } - } - // screen methods public static native void setDisplayMetrics(int screenWidthPixels, int screenHeightPixels, int availableLeftPixels, int availableTopPixels, diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java index f35db6436a..268a53044f 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceBase.java @@ -9,28 +9,27 @@ import android.os.IBinder; import android.util.Log; public class QtServiceBase extends Service { + private QtServiceDelegate m_delegate; - private final QtServiceDelegate m_delegate = new QtServiceDelegate(this); - QtServiceLoader m_loader = new QtServiceLoader(this); - protected void onCreateHook() { - // the application has already started - // do not reload everything again + @Override + public void onCreate() + { + super.onCreate(); + + m_delegate = new QtServiceDelegate(this); + + // the application has already started, do not reload everything again if (QtNative.isStarted()) { - m_loader = null; 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."); - } else { - m_loader.onCreate(); + return; } - } - @Override - public void onCreate() - { - super.onCreate(); - onCreateHook(); + QtServiceLoader loader = new QtServiceLoader(this); + loader.loadQtLibraries(); + QtNative.startApplication(loader.getApplicationParameters(), loader.getMainLibrary()); } @Override diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java index 1b6a8c9a8c..9791bf78fc 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceDelegate.java @@ -5,140 +5,16 @@ package org.qtproject.qt.android; import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; -import android.graphics.drawable.ColorDrawable; -import android.net.LocalServerSocket; -import android.net.LocalSocket; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.ResultReceiver; -import android.text.method.MetaKeyKeyListener; -import android.util.Base64; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.Surface; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; - -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileWriter; -import java.io.InputStreamReader; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Objects; - -import static org.qtproject.qt.android.QtConstants.*; public class QtServiceDelegate { - private String m_mainLib = null; private Service m_service = null; - private static String m_applicationParameters = null; QtServiceDelegate(Service service) { m_service = service; - } - - public boolean loadApplication(Service service, ClassLoader classLoader, Bundle loaderParams) - { - /// check parameters integrity - if (!loaderParams.containsKey(NATIVE_LIBRARIES_KEY) - || !loaderParams.containsKey(BUNDLED_LIBRARIES_KEY)) { - return false; - } - m_service = service; + // Set native context QtNative.setService(m_service, this); - QtNative.setClassLoader(classLoader); - - QtNative.setApplicationDisplayMetrics(10, 10, 0, 0, 10, 10, 120, 120, 1.0, 1.0, 60.0f); - - if (loaderParams.containsKey(STATIC_INIT_CLASSES_KEY)) { - for (String className : - Objects.requireNonNull(loaderParams.getStringArray(STATIC_INIT_CLASSES_KEY))) { - if (className.length() == 0) - continue; - try { - Class<?> initClass = classLoader.loadClass(className); - Object staticInitDataObject = initClass.newInstance(); // create an instance - try { - Method m = initClass.getMethod("setService", Service.class, Object.class); - m.invoke(staticInitDataObject, m_service, this); - } catch (Exception e) { - Log.d(QtNative.QtTAG, - "Class " + className + " does not implement setService method"); - } - - // For modules that don't need/have setService - try { - Method m = initClass.getMethod("setContext", Context.class); - m.invoke(staticInitDataObject, (Context)m_service); - } catch (Exception e) { - e.printStackTrace(); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - QtNative.loadQtLibraries(loaderParams.getStringArrayList(NATIVE_LIBRARIES_KEY)); - ArrayList<String> libraries = loaderParams.getStringArrayList(BUNDLED_LIBRARIES_KEY); - String nativeLibsDir = QtNativeLibrariesDir.nativeLibrariesDir(m_service); - QtNative.loadBundledLibraries(libraries, nativeLibsDir); - m_mainLib = loaderParams.getString(MAIN_LIBRARY_KEY); - - QtNative.setEnvironmentVariables(loaderParams.getString(ENVIRONMENT_VARIABLES_KEY)); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", - "Droid Sans Mono;Droid Sans;Droid Sans Fallback"); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif"); - QtNative.setEnvironmentVariable("HOME", m_service.getFilesDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("TMPDIR", m_service.getCacheDir().getAbsolutePath()); - QtNative.setEnvironmentVariable("QT_ANDROID_FONTS", - "Roboto;Droid Sans;Droid Sans Fallback"); - - if (loaderParams.containsKey(APPLICATION_PARAMETERS_KEY)) - m_applicationParameters = loaderParams.getString(APPLICATION_PARAMETERS_KEY); - else - m_applicationParameters = ""; - - m_mainLib = QtNative.loadMainLibrary(m_mainLib, nativeLibsDir); - return m_mainLib != null; - } - - public boolean startApplication() - { - // start application - try { - String nativeLibraryDir = QtNativeLibrariesDir.nativeLibrariesDir(m_service); - QtNative.startApplication(m_applicationParameters, m_mainLib); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } } } diff --git a/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java index f6add928e1..b045b9abff 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtServiceLoader.java @@ -5,35 +5,47 @@ package org.qtproject.qt.android; import android.app.Service; -import android.os.Bundle; import android.content.ComponentName; import android.content.pm.PackageManager; +import android.util.Log; -import java.security.Provider; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; public class QtServiceLoader extends QtLoader { - Service m_service; + private final Service m_service; public QtServiceLoader(Service service) { super(service); m_service = service; + + extractContextMetaData(); } - public void onCreate() { + @Override + protected void initContextInfo() { try { - m_contextInfo = m_service.getPackageManager().getServiceInfo(new ComponentName( - m_service, m_service.getClass()), PackageManager.GET_META_DATA); + m_contextInfo = m_service.getPackageManager().getServiceInfo( + new ComponentName(m_service, m_service.getClass()), + PackageManager.GET_META_DATA); } catch (Exception e) { e.printStackTrace(); - m_service.stopSelf(); - return; + finish(); } - - startApp(true); } @Override protected void finish() { m_service.stopSelf(); } + + @Override + protected void initStaticClassesImpl(Class<?> initClass, Object staticInitDataObject) { + try { + Method m = initClass.getMethod("setService", Service.class, Object.class); + m.invoke(staticInitDataObject, m_service, this); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + Log.d(QtTAG, "Class " + initClass.getName() + " does not implement setService method"); + } + } } |