Android: modularize and simplify QtLoader classes

The Qt loader is now expected to work in the following way:

QtActivity.onCreate()
--| QtActivityBase.onCreate()
----| Initiaze the delegate
----| Creates QtActivityLoader()
------| Creates QtLoader()
--------| Initialize the class loader
--------| Initialize static classes context
--------| Initialize Context Info
------| Sets common environment variables and parameters from metadata
------| Setup style extraction
------| Sets Activity specific metadata
--| Handles Activity themes
--| Call QtLoader.loadQtLibraries()
----| Fetch and load Qt native libraries
----| Fetch and load the app's main library
--| Start the Qt Native C++ app via the delegate

Few things are done in patch to simplify the Qt loader mechanism:

1)
Get rid of some unused methods, and move the loader instances
to be local instead of global.

2)
Split the awfully long methods in QtLoader into smaller methods for
readability and as preparation for next simplifications.

3)
Refactor Qt libraries loading code from the Delegate classes to
the Loader classes where it makes more sense to be at. At the same
time simplify some code into smaller logical blocks.

4)
The same boilerplate code for loading (with System.load()) Qt libs
and the main library was done twice between the Activity and Service
loaders, that is now done directly under the QtLoader. Same story
for initializing static Java classes with activity/service/context.

With this change All relevant Qt library loading logic is now under
QtLoader classes, and the latter have clear loader responsibilities
reflected in the code.

Task-number: QTBUG-115016
Change-Id: Ib76621d8beff4917c932456c5401ea4586942213
Reviewed-by: Tinja Paavoseppä <tinja.paavoseppa@qt.io>
bb10
Assam Boudjelthia 2023-10-25 11:40:17 +03:00
parent 7c59c5ed13
commit 3a505a859a
10 changed files with 737 additions and 856 deletions

View File

@ -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";
boolean darkModeFileMissing = !(new File(m_stylePath + "darkUiMode/style.json").exists());
m_missingDarkStyle = Build.VERSION.SDK_INT > 28 && darkModeFileMissing;
private static boolean isUiModeDark(Configuration config)
{
return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
}
m_missingNormalStyle = !(new File(m_stylePath + "style.json").exists());
public static String setup(Activity activity, String extractOption, int dpi) {
m_extractMinimal = loaderParams.containsKey(EXTRACT_STYLE_MINIMAL_KEY) &&
loaderParams.getBoolean(EXTRACT_STYLE_MINIMAL_KEY);
String dataDir = activity.getApplicationInfo().dataDir;
m_stylePath = dataDir + "/qt-reserved-files/android-style/" + dpi + "/";
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";
}
// 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) {

View File

@ -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

View File

@ -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 boolean loadApplication(Activity activity, ClassLoader classLoader, Bundle loaderParams)
{
/// check parameters integrity
if (!loaderParams.containsKey(NATIVE_LIBRARIES_KEY)
|| !loaderParams.containsKey(BUNDLED_LIBRARIES_KEY)
|| !loaderParams.containsKey(ENVIRONMENT_VARIABLES_KEY)) {
return false;
}
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;
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();
}
} 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_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;
}
}
public void onTerminate()
{
public void onTerminate() {
QtNative.terminateQt();
QtNative.m_qtThread.exit();
}
public void onCreate(Bundle savedInstanceState)
public void startNativeApplication(ArrayList<String> appParams, String mainLib)
{
if (m_surfaces != null)
return;
Runnable startApplication = new Runnable() {
@Override
public void run() {
try {
QtNative.startApplication(appParams, mainLib);
m_started = true;
} catch (Exception e) {
e.printStackTrace();
m_activity.finish();
}
}
};
initMembers(startApplication);
}
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

View File

@ -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;
extractContextMetaData();
}
@Override
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();
}
}
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
protected void finish() {
showErrorDialog();
m_activity.finish();
}
@Override
protected String getTitle() {
return (String) m_activity.getTitle();
protected void initStaticClassesImpl(Class<?> initClass, Object staticInitDataObject) {
try {
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");
}
}
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 runOnUiThread(Runnable run) {
m_activity.runOnUiThread(run);
}
protected void extractContextMetaData()
{
super.extractContextMetaData();
@Override
Intent getIntent() {
return m_activity.getIntent();
}
setEnvironmentVariable("QT_ANDROID_APP_ICON_SIZE", String.valueOf(getAppIconSize()));
public void onCreate(Bundle savedInstanceState) {
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();
setupStyleExtraction();
Intent intent = m_activity.getIntent();
if (intent == null) {
Log.w(QtTAG, "Null Intent from the current Activity.");
return;
}
try {
m_activity.setTheme(Class.forName("android.R$style").getDeclaredField(
QT_ANDROID_DEFAULT_THEME).getInt(null));
} catch (Exception e) {
e.printStackTrace();
}
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);
}
((QtActivityBase)m_activity).getActivityDelegate().onCreate(savedInstanceState);
String intentArgs = intent.getStringExtra("applicationArguments");
if (intentArgs != null)
setApplicationParameters(intentArgs);
Bundle extras = intent.getExtras();
if (extras == null) {
Log.w(QtTAG, "Null extras from the Activity's intent.");
return;
}
m_displayDensity = m_activity.getResources().getDisplayMetrics().densityDpi;
int flags = m_activity.getApplicationInfo().flags;
boolean isDebuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
ENVIRONMENT_VARIABLES += "\tQT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME
+ "/\tQT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + "\t";
if (isDebuggable) {
if (extras.containsKey("extraenvvars")) {
String extraEnvVars = extras.getString("extraenvvars");
setEnvironmentVariables(getDecodedUtfString(extraEnvVars));
}
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";
if (extras.containsKey("extraappparams")) {
String extraAppParams = extras.getString("extraappparams");
setApplicationParameters(getDecodedUtfString(extraAppParams));
}
} else {
ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1\t";
Log.d(QtNative.QtTAG, "Not in debug mode! It is not allowed to use extra arguments " +
"in non-debug mode.");
}
startApp(true);
}
}

View File

@ -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;

View File

@ -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";
protected static final String QtTAG = "QtLoader";
// These parameters matter in case of deploying application as system (embedded into firmware)
public static final String SYSTEM_LIB_PATH = "/system/lib/";
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 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.
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() {}
protected String getTitle() {
return "Qt";
}
protected void runOnUiThread(Runnable run) {
run.run();
}
Intent getIntent()
{
return null;
}
// Implement in subclass
private final List<String> supportedAbis = Arrays.asList(Build.SUPPORTED_ABIS);
private String preferredAbi = null;
private ArrayList<String> prefferedAbiLibs(String []libs)
{
HashMap<String, ArrayList<String>> abisLibs = new HashMap<>();
private ArrayList<String> preferredAbiLibs(String[] libs) {
HashMap<String, ArrayList<String>> abiLibs = new HashMap<>();
for (String lib : libs) {
String[] archLib = lib.split(";", 2);
if (preferredAbi != null && !archLib[0].equals(preferredAbi))
if (m_preferredAbi != null && !archLib[0].equals(m_preferredAbi))
continue;
if (!abisLibs.containsKey(archLib[0]))
abisLibs.put(archLib[0], new ArrayList<String>());
abisLibs.get(archLib[0]).add(archLib[1]);
if (!abiLibs.containsKey(archLib[0]))
abiLibs.put(archLib[0], new ArrayList<>());
Objects.requireNonNull(abiLibs.get(archLib[0])).add(archLib[1]);
}
if (preferredAbi != null) {
if (abisLibs.containsKey(preferredAbi)) {
return abisLibs.get(preferredAbi);
if (m_preferredAbi != null) {
if (abiLibs.containsKey(m_preferredAbi)) {
return abiLibs.get(m_preferredAbi);
}
return new ArrayList<String>();
return new ArrayList<>();
}
for (String abi: supportedAbis) {
if (abisLibs.containsKey(abi)) {
preferredAbi = abi;
return abisLibs.get(abi);
for (String abi : Build.SUPPORTED_ABIS) {
if (abiLibs.containsKey(abi)) {
m_preferredAbi = abi;
return abiLibs.get(abi);
}
}
return new ArrayList<String>();
return new ArrayList<>();
}
// this function is used to load and start the loader
private void loadApplication(Bundle loaderParams)
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");
}
}
}
/**
* 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();
m_classLoader = new DexClassLoader("", 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 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;
}
/**
* Adds a parameter string to the internal array list of parameters.
**/
public void setApplicationParameter(String param)
{
if (param == null || param.isEmpty())
return;
m_applicationParameters.add(param);
}
/**
* 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 param : params.split("\t"))
setApplicationParameter(param);
}
/**
* 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)
{
if (environmentVariables == null || environmentVariables.isEmpty())
return;
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");
}
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.
**/
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 metadata.getString(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);
return preferredAbiLibs(m_resources.getStringArray(id));
}
@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 (!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 {
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");
}
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.");
}
}
if (libsDir == null)
throw new Exception("");
if (m_qtLibs != null) {
String libPrefix = libsDir + "lib";
for (String lib : m_qtLibs)
libraryList.add(libPrefix + lib + ".so");
}
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;
}
Bundle loaderParams = new Bundle();
loaderParams.putString(DEX_PATH_KEY, new String());
id = resources.getIdentifier("static_init_classes", "string", packageName);
loaderParams.putStringArray(STATIC_INIT_CLASSES_KEY, resources.getString(id)
.split(":"));
loaderParams.putStringArrayList(NATIVE_LIBRARIES_KEY, libraryList);
String themePath = m_context.getApplicationInfo().dataDir + "/qt-reserved-files/android-style/";
String stylePath = themePath + m_displayDensity + "/";
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";
}
}
// 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";
}
}
if (!extractOption.equals("none")) {
loaderParams.putString(EXTRACT_STYLE_KEY, stylePath);
loaderParams.putBoolean(EXTRACT_STYLE_MINIMAL_KEY, extractOption.equals("minimal"));
}
if (extractOption.equals("full"))
ENVIRONMENT_VARIABLES += "\tQT_USE_ANDROID_NATIVE_STYLE=1";
ENVIRONMENT_VARIABLES += "\tANDROID_STYLE_PATH=" + stylePath;
if (m_contextInfo.metaData.containsKey("android.app.trace_location")) {
String loc = m_contextInfo.metaData.getString("android.app.trace_location");
ENVIRONMENT_VARIABLES += "\tQTRACE_LOCATION="+loc;
}
loaderParams.putString(ENVIRONMENT_VARIABLES_KEY, ENVIRONMENT_VARIABLES);
String appParams = null;
if (APPLICATION_PARAMETERS != null)
appParams = APPLICATION_PARAMETERS;
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_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 (appParams != null)
loaderParams.putString(APPLICATION_PARAMETERS_KEY, appParams);
loadApplication(loaderParams);
return;
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];
}
}

View File

@ -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,

View File

@ -9,28 +9,27 @@ import android.os.IBinder;
import android.util.Log;
public class QtServiceBase extends Service {
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
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();
}
}
private QtServiceDelegate m_delegate;
@Override
public void onCreate()
{
super.onCreate();
onCreateHook();
m_delegate = new QtServiceDelegate(this);
// the application has already started, do not reload everything again
if (QtNative.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;
}
QtServiceLoader loader = new QtServiceLoader(this);
loader.loadQtLibraries();
QtNative.startApplication(loader.getApplicationParameters(), loader.getMainLibrary());
}
@Override

View File

@ -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;
}
}
}

View File

@ -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");
}
}
}