Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
QtLoader.java
Go to the documentation of this file.
1// Copyright (C) 2023 The Qt Company Ltd.
2// Copyright (c) 2019, BogDan Vatra <bogdan@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:trusted-data-only
5
6package org.qtproject.qt.android;
7
8import android.annotation.SuppressLint;
9import android.app.Activity;
10import android.app.Service;
11import android.content.Intent;
12import android.content.ComponentName;
13import android.content.Context;
14import android.content.ContextWrapper;
15import android.content.pm.ApplicationInfo;
16import android.content.pm.PackageManager;
17import android.content.pm.ComponentInfo;
18import android.content.pm.PackageManager.NameNotFoundException;
19import android.content.res.Resources;
20import android.os.Build;
21import android.os.Bundle;
22import android.os.Debug;
23import android.os.Process;
24import android.system.Os;
25import android.util.Log;
26
27import java.io.File;
28import java.lang.IllegalArgumentException;
29import java.util.ArrayList;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.Objects;
33import java.util.HashSet;
34import java.util.Set;
35
36import dalvik.system.DexClassLoader;
37
38abstract class QtLoader {
39
40 protected static final String QtTAG = "QtLoader";
41
42 private final Resources m_resources;
43 private final String m_packageName;
44 private final String m_preferredAbi;
45 private String m_extractedNativeLibsDir = null;
47 private ClassLoader m_classLoader;
48
49 protected ComponentInfo m_contextInfo;
50
51 protected String m_mainLibPath;
52 protected String m_mainLibName;
53 protected String m_applicationParameters = "";
54 protected final HashMap<String, String> m_environmentVariables = new HashMap<>();
55
56 protected static QtLoader m_instance = null;
57 protected boolean m_librariesLoaded;
58
60
69 QtLoader(ContextWrapper context) throws IllegalArgumentException {
70 m_resources = context.getResources();
71 m_packageName = context.getPackageName();
72 final Context baseContext = context.getBaseContext();
73 if (!(baseContext instanceof Activity || baseContext instanceof Service)) {
74 throw new IllegalArgumentException("QtLoader: Context is not an instance of " +
75 "Activity or Service");
76 }
77
78 initClassLoader(baseContext);
79 try {
80 initContextInfo(baseContext);
81 } catch (NameNotFoundException e) {
82 throw new IllegalArgumentException("QtLoader: No ComponentInfo found for given " +
83 "Context", e);
84 }
85 m_preferredAbi = resolvePreferredAbi();
86 }
87
93 protected void initContextInfo(Context context) throws NameNotFoundException {
94 if (context instanceof Activity) {
95 m_contextInfo = context.getPackageManager().getActivityInfo(
96 ((Activity)context).getComponentName(), PackageManager.GET_META_DATA);
97 } else if (context instanceof Service) {
98 m_contextInfo = context.getPackageManager().getServiceInfo(
99 new ComponentName(context, context.getClass()),
100 PackageManager.GET_META_DATA);
101 }
102 }
103
109 protected void extractContextMetaData(Context context) {
110 setEnvironmentVariable("QT_ANDROID_FONTS", "Roboto;Droid Sans;Droid Sans Fallback");
111 String monospaceFonts = "Droid Sans Mono;Droid Sans;Droid Sans Fallback";
112 setEnvironmentVariable("QT_ANDROID_FONTS_MONOSPACE", monospaceFonts);
113 setEnvironmentVariable("QT_ANDROID_FONTS_SERIF", "Droid Serif");
114 setEnvironmentVariable("HOME", context.getFilesDir().getAbsolutePath());
115 setEnvironmentVariable("TMPDIR", context.getCacheDir().getAbsolutePath());
116 setEnvironmentVariable("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED", isBackgroundRunningBlocked());
117 setEnvironmentVariable("QTRACE_LOCATION", getMetaData("android.app.trace_location"));
118 appendApplicationParameters(getMetaData("android.app.arguments"));
119
120 if (context instanceof Activity) {
121 final int flags = context.getApplicationInfo().flags;
122 final boolean isDebuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
123 Intent intent = ((Activity) context).getIntent();
124 if (isDebuggable && intent != null && intent.hasExtra("applicationArguments"))
125 appendApplicationParameters(intent.getStringExtra("applicationArguments"));
126 }
127 }
128
129 private String isBackgroundRunningBlocked() {
130 final String backgroundRunning = getMetaData("android.app.background_running");
131 if (backgroundRunning.compareTo("true") == 0)
132 return "0";
133 return "1";
134 }
135
136 private ArrayList<String> preferredAbiLibs(String[] libs) {
137 ArrayList<String> abiLibs = new ArrayList<>();
138 for (String lib : libs) {
139 // We expect each line to be in the form "<abi>;<libName>"
140 String[] splits = lib.split(";", 2);
141 // Ensure we have both abi and lib name parts
142 if (splits == null || splits.length < 2)
143 continue;
144
145 // Ensure the lib name for the preferred abi is not empty
146 if (!splits[0].equals(m_preferredAbi) || splits[1].isEmpty())
147 continue;
148
149 abiLibs.add(splits[1]);
150 }
151
152 return abiLibs;
153 }
154
155 @SuppressLint("DiscouragedApi")
156 private String resolvePreferredAbi()
157 {
158 try {
159 int id = m_resources.getIdentifier("qt_libs", "array", m_packageName);
160 String[] libs = m_resources.getStringArray(id);
161 Set<String> uniqueAbis = new HashSet<>();
162
163 for (String lib : libs) {
164 String[] splits = lib.split(";", 2);
165 // Ensure we have both abi and lib name parts
166 if (splits.length < 2)
167 continue;
168
169 uniqueAbis.add(splits[0].trim());
170 }
171
172 String fallbackAbi = null;
173 boolean is64Bit = Process.is64Bit();
174 for (String abi : Build.SUPPORTED_ABIS) {
175 if (!uniqueAbis.contains(abi))
176 continue;
177
178 if (abi.contains("64") == is64Bit) // best match
179 return abi;
180
181 if (fallbackAbi == null)
182 fallbackAbi = abi;
183 }
184
185 if (fallbackAbi != null)
186 return fallbackAbi;
187
188 final String packagedAbis = "[" + String.join(", ", uniqueAbis) + "]";
189 final String deviceAbis = "[" + String.join(", ", Build.SUPPORTED_ABIS) + "]";
190 Log.w(QtTAG, "No packaged library ABIs " + packagedAbis + " match the device ABIs "
191 + deviceAbis + ", falling back to " + Build.SUPPORTED_ABIS[0] + ".");
192 } catch (Resources.NotFoundException ignored) { }
193
194 return Build.SUPPORTED_ABIS[0];
195 }
196
201 private void initClassLoader(Context context)
202 {
203 // directory where optimized DEX files should be written.
204 String outDexPath = context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();
205 String sourceDir = context.getApplicationInfo().sourceDir;
206 m_classLoader = new DexClassLoader(sourceDir, outDexPath, null, context.getClassLoader());
207 QtNative.setClassLoader(m_classLoader);
208 }
209
214 public String getMainLibraryPath() {
215 return m_mainLibPath;
216 }
217
225 public void setMainLibraryName(String libName) {
226 m_mainLibName = libName;
227 }
228
234 public String getApplicationParameters() {
235 return m_applicationParameters;
236 }
237
242 public void appendApplicationParameters(String params)
243 {
244 if (params == null || params.isEmpty())
245 return;
246
247 if (!m_applicationParameters.isEmpty())
248 m_applicationParameters += " ";
249 m_applicationParameters += params;
250 }
251
255 public void setEnvironmentVariable(String key, String value)
256 {
257 try {
258 android.system.Os.setenv(key, value, true);
259 m_environmentVariables.put(key, value);
260 } catch (Exception e) {
261 Log.e(QtTAG, "Could not set environment variable:" + key + "=" + value);
262 e.printStackTrace();
263 }
264 }
265
270 public void setEnvironmentVariables(String environmentVariables)
271 {
272 if (environmentVariables == null || environmentVariables.isEmpty())
273 return;
274
275 for (String variable : environmentVariables.split("\t")) {
276 String[] keyValue = variable.split("=", 2);
277 if (keyValue.length < 2 || keyValue[0].isEmpty())
278 continue;
279
280 setEnvironmentVariable(keyValue[0], keyValue[1]);
281 }
282 }
283
291 private void parseNativeLibrariesDir() {
292 if (m_contextInfo == null)
293 return;
294 if (isBundleQtLibs()) {
295 String nativeLibraryPrefix = m_contextInfo.applicationInfo.nativeLibraryDir + "/";
296 File nativeLibraryDir = new File(nativeLibraryPrefix);
297 if (nativeLibraryDir.exists()) {
298 String[] list = nativeLibraryDir.list();
299 if (nativeLibraryDir.isDirectory() && list != null && list.length > 0) {
300 m_extractedNativeLibsDir = nativeLibraryPrefix;
301 }
302 }
303 } else {
304 // First check if user has provided system libs prefix in AndroidManifest
305 String systemLibsPrefix = getApplicationMetaData("android.app.system_libs_prefix");
306
307 // If not, check if it's provided by androiddeployqt in libs.xml
308 if (systemLibsPrefix.isEmpty())
309 systemLibsPrefix = getSystemLibsPrefix();
310
311 if (systemLibsPrefix.isEmpty()) {
312 final String SYSTEM_LIB_PATH = "/system/lib/";
313 systemLibsPrefix = SYSTEM_LIB_PATH;
314 Log.e(QtTAG, "Using " + SYSTEM_LIB_PATH + " as default libraries path. "
315 + "It looks like the app is deployed using Unbundled "
316 + "deployment. It may be necessary to specify the path to "
317 + "the directory where Qt libraries are installed using either "
318 + "android.app.system_libs_prefix metadata variable in your "
319 + "AndroidManifest.xml or QT_ANDROID_SYSTEM_LIBS_PATH in your "
320 + "CMakeLists.txt");
321 }
322
323 File systemLibraryDir = new File(systemLibsPrefix);
324 String[] list = systemLibraryDir.list();
325 if (systemLibraryDir.exists()) {
326 if (systemLibraryDir.isDirectory() && list != null && list.length > 0)
327 m_extractedNativeLibsDir = systemLibsPrefix;
328 else
329 Log.e(QtTAG, "System library directory " + systemLibsPrefix + " is empty.");
330 } else {
331 Log.e(QtTAG, "System library directory " + systemLibsPrefix + " does not exist.");
332 }
333 }
334
335 if (m_extractedNativeLibsDir != null && !m_extractedNativeLibsDir.endsWith("/"))
336 m_extractedNativeLibsDir += "/";
337 }
338
343 private String getApplicationMetaData(String key) {
344 ApplicationInfo applicationInfo = m_contextInfo.applicationInfo;
345 if (applicationInfo == null)
346 return "";
347
348 Bundle metadata = applicationInfo.metaData;
349 if (metadata == null || !metadata.containsKey(key))
350 return "";
351
352 return metadata.getString(key);
353 }
354
358 protected String getMetaData(String key) {
359 if (m_contextInfo == null)
360 return "";
361
362 Bundle metadata = m_contextInfo.metaData;
363 if (metadata == null || !metadata.containsKey(key))
364 return "";
365
366 return String.valueOf(metadata.get(key));
367 }
368
369 @SuppressLint("DiscouragedApi")
370 private ArrayList<String> getQtLibrariesList() {
371 try {
372 int id = m_resources.getIdentifier("qt_libs", "array", m_packageName);
373 return preferredAbiLibs(m_resources.getStringArray(id));
374 } catch (Resources.NotFoundException ignored) {
375 return new ArrayList<>();
376 }
377 }
378
379 @SuppressLint("DiscouragedApi")
380 private boolean useLocalQtLibs() {
381 try {
382 int id = m_resources.getIdentifier("use_local_qt_libs", "string", m_packageName);
383 return Integer.parseInt(m_resources.getString(id)) == 1;
384 } catch (Resources.NotFoundException ignored) {
385 return false;
386 }
387 }
388
389 @SuppressLint("DiscouragedApi")
390 private boolean isBundleQtLibs() {
391 try {
392 int id = m_resources.getIdentifier("bundle_local_qt_libs", "string", m_packageName);
393 return Integer.parseInt(m_resources.getString(id)) == 1;
394 } catch (Resources.NotFoundException ignored) {
395 return false;
396 }
397 }
398
399 @SuppressLint("DiscouragedApi")
400 private String getSystemLibsPrefix() {
401 try {
402 int id = m_resources.getIdentifier("system_libs_prefix", "string", m_packageName);
403 return m_resources.getString(id);
404 } catch (Resources.NotFoundException ignored) {
405 return "";
406 }
407 }
408
409 @SuppressLint("DiscouragedApi")
410 private ArrayList<String> getLocalLibrariesList() {
411 ArrayList<String> localLibs = new ArrayList<>();
412 try {
413 int id = m_resources.getIdentifier("load_local_libs", "array", m_packageName);
414 for (String arrayItem : preferredAbiLibs(m_resources.getStringArray(id))) {
415 Collections.addAll(localLibs, arrayItem.split(":"));
416 }
417 } catch (Resources.NotFoundException ignored) { }
418 return localLibs;
419 }
420
421 @SuppressLint("DiscouragedApi")
422 private String[] getBundledLibs() {
423 try {
424 int id = m_resources.getIdentifier("bundled_libs", "array", m_packageName);
425 return m_resources.getStringArray(id);
426 } catch (Resources.NotFoundException ignored) {
427 return new String[0];
428 }
429 }
430
434 private static boolean isUncompressedNativeLibs()
435 {
436 Context context = QtNative.getContext();
437 if (context == null) {
438 Log.w(QtTAG, "isUncompressedNativeLibs() called before a valid context was set.");
439 return false;
440 }
441 int flags = context.getApplicationInfo().flags;
442 return (flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) == 0;
443 }
444
449 private String getApkNativeLibrariesDir()
450 {
451 String apkFilePath = QtApkFileEngine.getAppApkFilePath();
452 if (apkFilePath == null)
453 return null;
454 return apkFilePath + "!/lib/" + m_preferredAbi + "/";
455 }
456
462 public LoadingResult loadQtLibraries() {
463 if (m_librariesLoaded)
465
466 if (!useLocalQtLibs()) {
467 Log.w(QtTAG, "Use local Qt libs is false");
468 return LoadingResult.Failed;
469 }
470
471 if (isUncompressedNativeLibs()) {
472 String apkLibPath = getApkNativeLibrariesDir();
473 if (apkLibPath == null) {
474 Log.e(QtTAG, "Failed to resolve the APK native libraries directory");
475 return LoadingResult.Failed;
476 }
477 setEnvironmentVariable("QT_PLUGIN_PATH", apkLibPath);
478 setEnvironmentVariable("QML_PLUGIN_PATH", apkLibPath);
479 } else {
480 parseNativeLibrariesDir();
481 if (m_extractedNativeLibsDir == null || m_extractedNativeLibsDir.isEmpty()) {
482 Log.e(QtTAG, "The native libraries directory is null or empty");
483 return LoadingResult.Failed;
484 }
485 setEnvironmentVariable("QT_PLUGIN_PATH", m_extractedNativeLibsDir);
486 setEnvironmentVariable("QML_PLUGIN_PATH", m_extractedNativeLibsDir);
487 }
488
489 // Load native Qt APK libraries
490 ArrayList<String> nativeLibraries = getQtLibrariesList();
491 nativeLibraries.addAll(getLocalLibrariesList());
492
493 if (Debug.isDebuggerConnected()) {
494 final String debuggerSleepEnvVarName = "QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS";
495 int debuggerSleepMs = 3000;
496 if (Os.getenv(debuggerSleepEnvVarName) != null) {
497 try {
498 debuggerSleepMs = Integer.parseInt(Os.getenv(debuggerSleepEnvVarName));
499 } catch (NumberFormatException ignored) {
500 }
501 }
502
503 if (debuggerSleepMs > 0) {
504 Log.i(QtTAG, "Sleeping for " + debuggerSleepMs +
505 "ms, helping the native debugger to settle. " +
506 "Use the env " + debuggerSleepEnvVarName +
507 " variable to change this value.");
508 QtNative.getQtThread().sleep(debuggerSleepMs);
509 }
510 }
511
512 if (!loadLibraries(nativeLibraries)) {
513 Log.e(QtTAG, "Loading Qt native libraries failed");
514 return LoadingResult.Failed;
515 }
516
517 // add all bundled Qt libs to loader params
518 ArrayList<String> bundledLibraries = new ArrayList<>(preferredAbiLibs(getBundledLibs()));
519 if (!loadLibraries(bundledLibraries)) {
520 Log.e(QtTAG, "Loading Qt bundled libraries failed");
521 return LoadingResult.Failed;
522 }
523
524 if (m_mainLibName == null)
525 m_mainLibName = getMetaData("android.app.lib_name");
526
527 if (m_mainLibName == null || m_mainLibName.isEmpty()) {
528 Log.e(QtTAG, "The main library name is null or empty.");
529 return LoadingResult.Failed;
530 }
531
532 // Load main lib
533 if (!loadMainLibrary(m_mainLibName + "_" + m_preferredAbi)) {
534 Log.e(QtTAG, "Loading main library failed");
535 return LoadingResult.Failed;
536 }
537 m_librariesLoaded = true;
539 }
540
541 // Loading libraries using System.load() uses full lib paths
542 // or System.loadLibrary() for uncompressed libs
543 @SuppressLint("UnsafeDynamicallyLoadedCode")
544 private String loadLibraryHelper(String library)
545 {
546 String loadedLib = null;
547 try {
548 File libFile = new File(library);
549 if (library.startsWith("/")) {
550 if (libFile.exists()) {
551 System.load(library);
552 loadedLib = library;
553 } else {
554 Log.e(QtTAG, "Can't find '" + library + "'");
555 }
556 } else {
557 System.loadLibrary(library);
558 loadedLib = library;
559 }
560 } catch (Exception | UnsatisfiedLinkError e) {
561 Log.e(QtTAG, "Can't load '" + library + "'", e);
562 }
563
564 return loadedLib;
565 }
566
570 private ArrayList<String> getLibrariesFullPaths(final ArrayList<String> libraries)
571 {
572 if (libraries == null)
573 return null;
574
575 ArrayList<String> absolutePathLibraries = new ArrayList<>();
576 for (String libName : libraries) {
577 // Add lib and .so to the lib name only if it doesn't already end with .so,
578 // this means some names don't necessarily need to have the lib prefix
579 if (isUncompressedNativeLibs()) {
580 if (libName.endsWith(".so"))
581 libName = libName.substring(3, libName.length() - 3);
582 absolutePathLibraries.add(libName);
583 } else {
584 if (!libName.endsWith(".so"))
585 libName = "lib" + libName + ".so";
586 File file = new File(m_extractedNativeLibsDir + libName);
587 absolutePathLibraries.add(file.getAbsolutePath());
588 }
589 }
590
591 return absolutePathLibraries;
592 }
593
600 private boolean loadMainLibrary(String mainLibName)
601 {
602 ArrayList<String> oneEntryArray = new ArrayList<>(Collections.singletonList(mainLibName));
603 String mainLibPath = getLibrariesFullPaths(oneEntryArray).get(0);
604 QtNative.getQtThread().run(() -> {
605 m_mainLibPath = loadLibraryHelper(mainLibPath);
606 if (m_mainLibPath != null && isUncompressedNativeLibs())
607 m_mainLibPath = getApkNativeLibrariesDir() + "lib" + m_mainLibPath + ".so";
608 });
609
610 return m_mainLibPath != null;
611 }
612
618 @SuppressWarnings("BooleanMethodIsAlwaysInverted")
619 private boolean loadLibraries(final ArrayList<String> libraries)
620 {
621 if (libraries == null)
622 return false;
623
624 ArrayList<String> fullPathLibs = getLibrariesFullPaths(libraries);
625
626 if (libraries.size() != fullPathLibs.size()) {
627 Log.e(QtTAG, "Failed to get full paths of libraries.");
628 return false;
629 }
630
631 final boolean[] success = {true};
632 QtNative.getQtThread().run(() -> {
633 for (int i = 0; i < fullPathLibs.size(); ++i) {
634 String libName = fullPathLibs.get(i);
635 if (loadLibraryHelper(libName) == null) {
636 success[0] = false;
637 break;
638 }
639 }
640 });
641
642 return success[0];
643 }
644}
constexpr qsizetype length() const noexcept
Definition qlist.h:465
QPainter Context
static const QString context()
Definition java.cpp:396
QMap< Name, StatePointer > Bundle
Definition lalr.h:46
std::string trim(std::string const &str)
Returns a new string without whitespace at the start/end.
EGLOutputLayerEXT EGLint EGLAttrib value
[3]
GLvoid ** params
GLuint64 key
GLbitfield flags
GLenum GLenum variable
decltype(openFileForWriting({})) File
Definition main.cpp:76
QList< int > list
[14]