diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f626334dbd..caea9c2939 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ libraryList = new ArrayList<>(); + String localPrefix = m_context.getApplicationInfo().dataDir + "/"; + String pluginsPrefix = localPrefix + "qt-reserved-files/"; + cleanOldCacheIfNecessary(localPrefix, pluginsPrefix); + extractBundledPluginsAndImports(pluginsPrefix); + + for (String lib : m_qtLibs) { + libraryList.add(localPrefix + "lib/lib" + lib + ".so"); + } + + if (m_contextInfo.metaData.containsKey("android.app.load_local_libs")) { + String[] extraLibs = m_contextInfo.metaData.getString("android.app.load_local_libs").split(":"); + for (String lib : extraLibs) { + if (lib.length() > 0) { + if (lib.startsWith("lib/")) { + libraryList.add(localPrefix + lib); + } else { + libraryList.add(pluginsPrefix + lib); + } + } + } + } + + Bundle loaderParams = new Bundle(); + loaderParams.putString(DEX_PATH_KEY, new String()); + + loaderParams.putStringArrayList(NATIVE_LIBRARIES_KEY, libraryList); + + loaderParams.putString(ENVIRONMENT_VARIABLES_KEY, ENVIRONMENT_VARIABLES + + "\tQML2_IMPORT_PATH=" + pluginsPrefix + "/qml" + + "\tQML_IMPORT_PATH=" + pluginsPrefix + "/imports" + + "\tQT_PLUGIN_PATH=" + pluginsPrefix + "/plugins"); + + + // add all bundled Qt libs to loader params + ArrayList libs = new ArrayList<>(); + + String libName = m_contextInfo.metaData.getString("android.app.lib_name"); + 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 + + Class loaderClass = classLoader.loadClass(loaderClassName()); // load QtLoader class + Object qtLoader = loaderClass.newInstance(); // create an instance + Method prepareAppMethod = qtLoader.getClass().getMethod("loadApplication", + contextClassName(), + ClassLoader.class, + Bundle.class); + prepareAppMethod.invoke(qtLoader, m_context, classLoader, loaderParams); + + // now load the application library so it's accessible from this class loader + if (libName != null) { + System.loadLibrary(libName); + } + } catch (Exception e) { + Log.e(QtApplication.QtTAG, "Error pre-loading HiFi Qt app", e); + } + } + + protected String loaderClassName() { + return "org.qtproject.qt5.android.QtActivityDelegate"; + } + + protected Class contextClassName() { + return android.app.Activity.class; + } + + + private void deleteRecursively(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteRecursively(file); + } else { + file.delete(); + } + } + + directory.delete(); + } + } + + private void cleanOldCacheIfNecessary(String oldLocalPrefix, String localPrefix) { + File newCache = new File(localPrefix); + if (!newCache.exists()) { + { + File oldPluginsCache = new File(oldLocalPrefix + "plugins/"); + if (oldPluginsCache.exists() && oldPluginsCache.isDirectory()) { + deleteRecursively(oldPluginsCache); + } + } + + { + File oldImportsCache = new File(oldLocalPrefix + "imports/"); + if (oldImportsCache.exists() && oldImportsCache.isDirectory()) { + deleteRecursively(oldImportsCache); + } + } + + { + File oldQmlCache = new File(oldLocalPrefix + "qml/"); + if (oldQmlCache.exists() && oldQmlCache.isDirectory()) { + deleteRecursively(oldQmlCache); + } + } + } + } + + static private void copyFile(InputStream inputStream, OutputStream outputStream) + throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + + int count; + while ((count = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + } + } + + private void copyAsset(String source, String destination) + throws IOException { + // Already exists, we don't have to do anything + File destinationFile = new File(destination); + if (destinationFile.exists()) { + return; + } + + File parentDirectory = destinationFile.getParentFile(); + if (!parentDirectory.exists()) { + parentDirectory.mkdirs(); + } + + destinationFile.createNewFile(); + + AssetManager assetsManager = m_context.getAssets(); + InputStream inputStream = assetsManager.open(source); + OutputStream outputStream = new FileOutputStream(destinationFile); + copyFile(inputStream, outputStream); + + inputStream.close(); + outputStream.close(); + } + + private static void createBundledBinary(String source, String destination) + throws IOException { + // Already exists, we don't have to do anything + File destinationFile = new File(destination); + if (destinationFile.exists()) { + return; + } + + File parentDirectory = destinationFile.getParentFile(); + if (!parentDirectory.exists()) { + parentDirectory.mkdirs(); + } + + destinationFile.createNewFile(); + + InputStream inputStream = new FileInputStream(source); + OutputStream outputStream = new FileOutputStream(destinationFile); + copyFile(inputStream, outputStream); + + inputStream.close(); + outputStream.close(); + } + + private boolean cleanCacheIfNecessary(String pluginsPrefix, long packageVersion) { + File versionFile = new File(pluginsPrefix + "cache.version"); + + long cacheVersion = 0; + if (versionFile.exists() && versionFile.canRead()) { + try { + DataInputStream inputStream = new DataInputStream(new FileInputStream(versionFile)); + cacheVersion = inputStream.readLong(); + inputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (cacheVersion != packageVersion) { + deleteRecursively(new File(pluginsPrefix)); + return true; + } else { + return false; + } + } + + private void extractBundledPluginsAndImports(String pluginsPrefix) throws IOException { + String libsDir = m_context.getApplicationInfo().nativeLibraryDir + "/"; + long packageVersion = -1; + try { + PackageInfo packageInfo = m_context.getPackageManager().getPackageInfo(m_context.getPackageName(), 0); + packageVersion = packageInfo.lastUpdateTime; + } catch (Exception e) { + e.printStackTrace(); + } + + + if (!cleanCacheIfNecessary(pluginsPrefix, packageVersion)) { + return; + } + + { + File versionFile = new File(pluginsPrefix + "cache.version"); + + File parentDirectory = versionFile.getParentFile(); + if (!parentDirectory.exists()) { + parentDirectory.mkdirs(); + } + + versionFile.createNewFile(); + + DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(versionFile)); + outputStream.writeLong(packageVersion); + outputStream.close(); + } + + { + String key = BUNDLED_IN_LIB_RESOURCE_ID_KEY; + if (m_contextInfo.metaData.containsKey(key)) { + String[] list = m_context.getResources().getStringArray(m_contextInfo.metaData.getInt(key)); + + for (String bundledImportBinary : list) { + String[] split = bundledImportBinary.split(":"); + String sourceFileName = libsDir + split[0]; + String destinationFileName = pluginsPrefix + split[1]; + createBundledBinary(sourceFileName, destinationFileName); + } + } + } + + { + String key = BUNDLED_IN_ASSETS_RESOURCE_ID_KEY; + if (m_contextInfo.metaData.containsKey(key)) { + String[] list = m_context.getResources().getStringArray(m_contextInfo.metaData.getInt(key)); + + for (String fileName : list) { + String[] split = fileName.split(":"); + String sourceFileName = split[0]; + String destinationFileName = pluginsPrefix + split[1]; + copyAsset(sourceFileName, destinationFileName); + } + } + + } + } +}