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 Intent intent = ((Activity) context).getIntent();
122 if (intent != null)
123 appendApplicationParameters(intent.getStringExtra("applicationArguments"));
124 }
125 }
126
127 private String isBackgroundRunningBlocked() {
128 final String backgroundRunning = getMetaData("android.app.background_running");
129 if (backgroundRunning.compareTo("true") == 0)
130 return "0";
131 return "1";
132 }
133
134 private ArrayList<String> preferredAbiLibs(String[] libs) {
135 ArrayList<String> abiLibs = new ArrayList<>();
136 for (String lib : libs) {
137 // We expect each line to be in the form "<abi>;<libName>"
138 String[] splits = lib.split(";", 2);
139 // Ensure we have both abi and lib name parts
140 if (splits == null || splits.length < 2)
141 continue;
142
143 // Ensure the lib name for the preferred abi is not empty
144 if (!splits[0].equals(m_preferredAbi) || splits[1].isEmpty())
145 continue;
146
147 abiLibs.add(splits[1]);
148 }
149
150 return abiLibs;
151 }
152
153 @SuppressLint("DiscouragedApi")
154 private String resolvePreferredAbi()
155 {
156 try {
157 int id = m_resources.getIdentifier("qt_libs", "array", m_packageName);
158 String[] libs = m_resources.getStringArray(id);
159 Set<String> uniqueAbis = new HashSet<>();
160
161 for (String lib : libs) {
162 String[] splits = lib.split(";", 2);
163 // Ensure we have both abi and lib name parts
164 if (splits.length < 2)
165 continue;
166
167 uniqueAbis.add(splits[0].trim());
168 }
169
170 String fallbackAbi = null;
171 boolean is64Bit = Process.is64Bit();
172 for (String abi : Build.SUPPORTED_ABIS) {
173 if (!uniqueAbis.contains(abi))
174 continue;
175
176 if (abi.contains("64") == is64Bit) // best match
177 return abi;
178
179 if (fallbackAbi == null)
180 fallbackAbi = abi;
181 }
182
183 if (fallbackAbi != null)
184 return fallbackAbi;
185
186 final String packagedAbis = "[" + String.join(", ", uniqueAbis) + "]";
187 final String deviceAbis = "[" + String.join(", ", Build.SUPPORTED_ABIS) + "]";
188 Log.w(QtTAG, "No packaged library ABIs " + packagedAbis + " match the device ABIs "
189 + deviceAbis + ", falling back to " + Build.SUPPORTED_ABIS[0] + ".");
190 } catch (Resources.NotFoundException ignored) { }
191
192 return Build.SUPPORTED_ABIS[0];
193 }
194
199 private void initClassLoader(Context context)
200 {
201 // directory where optimized DEX files should be written.
202 String outDexPath = context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();
203 String sourceDir = context.getApplicationInfo().sourceDir;
204 m_classLoader = new DexClassLoader(sourceDir, outDexPath, null, context.getClassLoader());
205 QtNative.setClassLoader(m_classLoader);
206 }
207
212 public String getMainLibraryPath() {
213 return m_mainLibPath;
214 }
215
223 public void setMainLibraryName(String libName) {
224 m_mainLibName = libName;
225 }
226
232 public String getApplicationParameters() {
233 return m_applicationParameters;
234 }
235
240 public void appendApplicationParameters(String params)
241 {
242 if (params == null || params.isEmpty())
243 return;
244
245 if (!m_applicationParameters.isEmpty())
246 m_applicationParameters += " ";
247 m_applicationParameters += params;
248 }
249
253 public void setEnvironmentVariable(String key, String value)
254 {
255 try {
256 android.system.Os.setenv(key, value, true);
257 m_environmentVariables.put(key, value);
258 } catch (Exception e) {
259 Log.e(QtTAG, "Could not set environment variable:" + key + "=" + value);
260 e.printStackTrace();
261 }
262 }
263
268 public void setEnvironmentVariables(String environmentVariables)
269 {
270 if (environmentVariables == null || environmentVariables.isEmpty())
271 return;
272
273 for (String variable : environmentVariables.split("\t")) {
274 String[] keyValue = variable.split("=", 2);
275 if (keyValue.length < 2 || keyValue[0].isEmpty())
276 continue;
277
278 setEnvironmentVariable(keyValue[0], keyValue[1]);
279 }
280 }
281
289 private void parseNativeLibrariesDir() {
290 if (m_contextInfo == null)
291 return;
292 if (isBundleQtLibs()) {
293 String nativeLibraryPrefix = m_contextInfo.applicationInfo.nativeLibraryDir + "/";
294 File nativeLibraryDir = new File(nativeLibraryPrefix);
295 if (nativeLibraryDir.exists()) {
296 String[] list = nativeLibraryDir.list();
297 if (nativeLibraryDir.isDirectory() && list != null && list.length > 0) {
298 m_extractedNativeLibsDir = nativeLibraryPrefix;
299 }
300 }
301 } else {
302 // First check if user has provided system libs prefix in AndroidManifest
303 String systemLibsPrefix = getApplicationMetaData("android.app.system_libs_prefix");
304
305 // If not, check if it's provided by androiddeployqt in libs.xml
306 if (systemLibsPrefix.isEmpty())
307 systemLibsPrefix = getSystemLibsPrefix();
308
309 if (systemLibsPrefix.isEmpty()) {
310 final String SYSTEM_LIB_PATH = "/system/lib/";
311 systemLibsPrefix = SYSTEM_LIB_PATH;
312 Log.e(QtTAG, "Using " + SYSTEM_LIB_PATH + " as default libraries path. "
313 + "It looks like the app is deployed using Unbundled "
314 + "deployment. It may be necessary to specify the path to "
315 + "the directory where Qt libraries are installed using either "
316 + "android.app.system_libs_prefix metadata variable in your "
317 + "AndroidManifest.xml or QT_ANDROID_SYSTEM_LIBS_PATH in your "
318 + "CMakeLists.txt");
319 }
320
321 File systemLibraryDir = new File(systemLibsPrefix);
322 String[] list = systemLibraryDir.list();
323 if (systemLibraryDir.exists()) {
324 if (systemLibraryDir.isDirectory() && list != null && list.length > 0)
325 m_extractedNativeLibsDir = systemLibsPrefix;
326 else
327 Log.e(QtTAG, "System library directory " + systemLibsPrefix + " is empty.");
328 } else {
329 Log.e(QtTAG, "System library directory " + systemLibsPrefix + " does not exist.");
330 }
331 }
332
333 if (m_extractedNativeLibsDir != null && !m_extractedNativeLibsDir.endsWith("/"))
334 m_extractedNativeLibsDir += "/";
335 }
336
341 private String getApplicationMetaData(String key) {
342 ApplicationInfo applicationInfo = m_contextInfo.applicationInfo;
343 if (applicationInfo == null)
344 return "";
345
346 Bundle metadata = applicationInfo.metaData;
347 if (metadata == null || !metadata.containsKey(key))
348 return "";
349
350 return metadata.getString(key);
351 }
352
356 protected String getMetaData(String key) {
357 if (m_contextInfo == null)
358 return "";
359
360 Bundle metadata = m_contextInfo.metaData;
361 if (metadata == null || !metadata.containsKey(key))
362 return "";
363
364 return String.valueOf(metadata.get(key));
365 }
366
367 @SuppressLint("DiscouragedApi")
368 private ArrayList<String> getQtLibrariesList() {
369 try {
370 int id = m_resources.getIdentifier("qt_libs", "array", m_packageName);
371 return preferredAbiLibs(m_resources.getStringArray(id));
372 } catch (Resources.NotFoundException ignored) {
373 return new ArrayList<>();
374 }
375 }
376
377 @SuppressLint("DiscouragedApi")
378 private boolean useLocalQtLibs() {
379 try {
380 int id = m_resources.getIdentifier("use_local_qt_libs", "string", m_packageName);
381 return Integer.parseInt(m_resources.getString(id)) == 1;
382 } catch (Resources.NotFoundException ignored) {
383 return false;
384 }
385 }
386
387 @SuppressLint("DiscouragedApi")
388 private boolean isBundleQtLibs() {
389 try {
390 int id = m_resources.getIdentifier("bundle_local_qt_libs", "string", m_packageName);
391 return Integer.parseInt(m_resources.getString(id)) == 1;
392 } catch (Resources.NotFoundException ignored) {
393 return false;
394 }
395 }
396
397 @SuppressLint("DiscouragedApi")
398 private String getSystemLibsPrefix() {
399 try {
400 int id = m_resources.getIdentifier("system_libs_prefix", "string", m_packageName);
401 return m_resources.getString(id);
402 } catch (Resources.NotFoundException ignored) {
403 return "";
404 }
405 }
406
407 @SuppressLint("DiscouragedApi")
408 private ArrayList<String> getLocalLibrariesList() {
409 ArrayList<String> localLibs = new ArrayList<>();
410 try {
411 int id = m_resources.getIdentifier("load_local_libs", "array", m_packageName);
412 for (String arrayItem : preferredAbiLibs(m_resources.getStringArray(id))) {
413 Collections.addAll(localLibs, arrayItem.split(":"));
414 }
415 } catch (Resources.NotFoundException ignored) { }
416 return localLibs;
417 }
418
419 @SuppressLint("DiscouragedApi")
420 private String[] getBundledLibs() {
421 try {
422 int id = m_resources.getIdentifier("bundled_libs", "array", m_packageName);
423 return m_resources.getStringArray(id);
424 } catch (Resources.NotFoundException ignored) {
425 return new String[0];
426 }
427 }
428
432 private static boolean isUncompressedNativeLibs()
433 {
434 Context context = QtNative.getContext();
435 if (context == null) {
436 Log.w(QtTAG, "isUncompressedNativeLibs() called before a valid context was set.");
437 return false;
438 }
439 int flags = context.getApplicationInfo().flags;
440 return (flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) == 0;
441 }
442
447 private String getApkNativeLibrariesDir()
448 {
449 String apkFilePath = QtApkFileEngine.getAppApkFilePath();
450 if (apkFilePath == null)
451 return null;
452 return apkFilePath + "!/lib/" + m_preferredAbi + "/";
453 }
454
460 public LoadingResult loadQtLibraries() {
461 if (m_librariesLoaded)
463
464 if (!useLocalQtLibs()) {
465 Log.w(QtTAG, "Use local Qt libs is false");
466 return LoadingResult.Failed;
467 }
468
469 if (isUncompressedNativeLibs()) {
470 String apkLibPath = getApkNativeLibrariesDir();
471 if (apkLibPath == null) {
472 Log.e(QtTAG, "Failed to resolve the APK native libraries directory");
473 return LoadingResult.Failed;
474 }
475 setEnvironmentVariable("QT_PLUGIN_PATH", apkLibPath);
476 setEnvironmentVariable("QML_PLUGIN_PATH", apkLibPath);
477 } else {
478 parseNativeLibrariesDir();
479 if (m_extractedNativeLibsDir == null || m_extractedNativeLibsDir.isEmpty()) {
480 Log.e(QtTAG, "The native libraries directory is null or empty");
481 return LoadingResult.Failed;
482 }
483 setEnvironmentVariable("QT_PLUGIN_PATH", m_extractedNativeLibsDir);
484 setEnvironmentVariable("QML_PLUGIN_PATH", m_extractedNativeLibsDir);
485 }
486
487 // Load native Qt APK libraries
488 ArrayList<String> nativeLibraries = getQtLibrariesList();
489 nativeLibraries.addAll(getLocalLibrariesList());
490
491 if (Debug.isDebuggerConnected()) {
492 final String debuggerSleepEnvVarName = "QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS";
493 int debuggerSleepMs = 3000;
494 if (Os.getenv(debuggerSleepEnvVarName) != null) {
495 try {
496 debuggerSleepMs = Integer.parseInt(Os.getenv(debuggerSleepEnvVarName));
497 } catch (NumberFormatException ignored) {
498 }
499 }
500
501 if (debuggerSleepMs > 0) {
502 Log.i(QtTAG, "Sleeping for " + debuggerSleepMs +
503 "ms, helping the native debugger to settle. " +
504 "Use the env " + debuggerSleepEnvVarName +
505 " variable to change this value.");
506 QtNative.getQtThread().sleep(debuggerSleepMs);
507 }
508 }
509
510 if (!loadLibraries(nativeLibraries)) {
511 Log.e(QtTAG, "Loading Qt native libraries failed");
512 return LoadingResult.Failed;
513 }
514
515 // add all bundled Qt libs to loader params
516 ArrayList<String> bundledLibraries = new ArrayList<>(preferredAbiLibs(getBundledLibs()));
517 if (!loadLibraries(bundledLibraries)) {
518 Log.e(QtTAG, "Loading Qt bundled libraries failed");
519 return LoadingResult.Failed;
520 }
521
522 if (m_mainLibName == null)
523 m_mainLibName = getMetaData("android.app.lib_name");
524
525 if (m_mainLibName == null || m_mainLibName.isEmpty()) {
526 Log.e(QtTAG, "The main library name is null or empty.");
527 return LoadingResult.Failed;
528 }
529
530 // Load main lib
531 if (!loadMainLibrary(m_mainLibName + "_" + m_preferredAbi)) {
532 Log.e(QtTAG, "Loading main library failed");
533 return LoadingResult.Failed;
534 }
535 m_librariesLoaded = true;
537 }
538
539 // Loading libraries using System.load() uses full lib paths
540 // or System.loadLibrary() for uncompressed libs
541 @SuppressLint("UnsafeDynamicallyLoadedCode")
542 private String loadLibraryHelper(String library)
543 {
544 String loadedLib = null;
545 try {
546 File libFile = new File(library);
547 if (library.startsWith("/")) {
548 if (libFile.exists()) {
549 System.load(library);
550 loadedLib = library;
551 } else {
552 Log.e(QtTAG, "Can't find '" + library + "'");
553 }
554 } else {
555 System.loadLibrary(library);
556 loadedLib = library;
557 }
558 } catch (Exception | UnsatisfiedLinkError e) {
559 Log.e(QtTAG, "Can't load '" + library + "'", e);
560 }
561
562 return loadedLib;
563 }
564
568 private ArrayList<String> getLibrariesFullPaths(final ArrayList<String> libraries)
569 {
570 if (libraries == null)
571 return null;
572
573 ArrayList<String> absolutePathLibraries = new ArrayList<>();
574 for (String libName : libraries) {
575 // Add lib and .so to the lib name only if it doesn't already end with .so,
576 // this means some names don't necessarily need to have the lib prefix
577 if (isUncompressedNativeLibs()) {
578 if (libName.endsWith(".so"))
579 libName = libName.substring(3, libName.length() - 3);
580 absolutePathLibraries.add(libName);
581 } else {
582 if (!libName.endsWith(".so"))
583 libName = "lib" + libName + ".so";
584 File file = new File(m_extractedNativeLibsDir + libName);
585 absolutePathLibraries.add(file.getAbsolutePath());
586 }
587 }
588
589 return absolutePathLibraries;
590 }
591
598 private boolean loadMainLibrary(String mainLibName)
599 {
600 ArrayList<String> oneEntryArray = new ArrayList<>(Collections.singletonList(mainLibName));
601 String mainLibPath = getLibrariesFullPaths(oneEntryArray).get(0);
602 QtNative.getQtThread().run(() -> {
603 m_mainLibPath = loadLibraryHelper(mainLibPath);
604 if (m_mainLibPath != null && isUncompressedNativeLibs())
605 m_mainLibPath = getApkNativeLibrariesDir() + "lib" + m_mainLibPath + ".so";
606 });
607
608 return m_mainLibPath != null;
609 }
610
616 @SuppressWarnings("BooleanMethodIsAlwaysInverted")
617 private boolean loadLibraries(final ArrayList<String> libraries)
618 {
619 if (libraries == null)
620 return false;
621
622 ArrayList<String> fullPathLibs = getLibrariesFullPaths(libraries);
623
624 if (libraries.size() != fullPathLibs.size()) {
625 Log.e(QtTAG, "Failed to get full paths of libraries.");
626 return false;
627 }
628
629 final boolean[] success = {true};
630 QtNative.getQtThread().run(() -> {
631 for (int i = 0; i < fullPathLibs.size(); ++i) {
632 String libName = fullPathLibs.get(i);
633 if (loadLibraryHelper(libName) == null) {
634 success[0] = false;
635 break;
636 }
637 }
638 });
639
640 return success[0];
641 }
642}
constexpr qsizetype length() const noexcept
Definition qlist.h:464
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]
GLuint64 key
GLbitfield flags
void ** params
GLenum GLenum variable
decltype(openFileForWriting({})) File
Definition main.cpp:76
QList< int > list
[14]