Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TIMOB-25597] Android: Fixed bug where multidexed apps crash on startup on Android 4.x #9694

Merged
merged 8 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
98 changes: 66 additions & 32 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,7 @@ AndroidBuilder.prototype.run = function run(logger, config, cli, finished) {
'processTiSymbols',
'copyModuleResources',
'removeOldFiles',
'copyGradleTemplate',
'generateJavaFiles',
'generateAidl',

Expand Down Expand Up @@ -3235,6 +3236,23 @@ AndroidBuilder.prototype.removeOldFiles = function removeOldFiles(next) {
next();
};

AndroidBuilder.prototype.copyGradleTemplate = function copyGradleTemplate(next) {
// Copy Titanium's ProGuard gradle script to the app's build directory.
afs.copyFileSync(path.join(this.templatesDir, 'proguard.gradle'), this.buildDir, { logger: this.logger.debug });

// Copy the gradle template directory tree to the app's build directory.
// Note: The copy function does not copy file permissions. So, we must re-add execute permissions.
// 0o755 = User Read/Write/Exec, Group Read/Execute, Others Read/Execute
afs.copyDirSyncRecursive(path.join(this.platformPath, 'templates', 'gradle'), this.buildDir, {
logger: this.logger.debug,
preserve: false
});
fs.chmodSync(path.join(this.buildDir, 'gradlew'), 0o755);
fs.chmodSync(path.join(this.buildDir, 'gradlew.bat'), 0o755);

next();
};

AndroidBuilder.prototype.generateJavaFiles = function generateJavaFiles(next) {
if (!this.forceRebuild) {
return next();
Expand Down Expand Up @@ -4092,8 +4110,11 @@ AndroidBuilder.prototype.runProguard = function runProguard(next) {
});

proguardHook(
this.jdkInfo.executables.java,
[ '-jar', this.androidInfo.sdk.proguard, '@' + proguardConfigFile ],
path.join(this.buildDir, (process.platform === 'win32') ? 'gradlew.bat' : 'gradlew'),
[
'-b', path.join(this.buildDir, 'proguard.gradle'),
'-Pconfiguration=' + proguardConfigFile
],
{ cwd: this.buildDir },
next
);
Expand All @@ -4117,13 +4138,15 @@ AndroidBuilder.prototype.runDexer = function runDexer(next) {
done();
}.bind(this));
}),
injars = [
injarsCore = [
this.buildBinClassesDir,
path.join(this.platformPath, 'lib', 'titanium-verify.jar')
].concat(Object.keys(this.moduleJars)).concat(Object.keys(this.jarLibraries)),
].concat(Object.keys(this.jarLibraries)),
injarsAll = injarsCore.slice().concat(Object.keys(this.moduleJars)),
shrinkedAndroid = path.join(path.dirname(this.androidInfo.sdk.dx), 'shrinkedAndroid.jar'),
baserules = path.join(path.dirname(this.androidInfo.sdk.dx), '..', 'mainDexClasses.rules'),
outjar = path.join(this.buildDir, 'mainDexClasses.jar');
outjar = path.join(this.buildDir, 'mainDexClasses.jar'),
pathArraySeparator = (process.platform === 'win32') ? ';' : ':';
let dexArgs = [
'-Xmx' + this.dxMaxMemory,
'-XX:-UseGCOverheadLimit',
Expand All @@ -4141,11 +4164,11 @@ AndroidBuilder.prototype.runDexer = function runDexer(next) {
}

if (this.allowDebugging && this.debugPort) {
injars.push(path.join(this.platformPath, 'lib', 'titanium-debug.jar'));
injarsAll.push(path.join(this.platformPath, 'lib', 'titanium-debug.jar'));
}

if (this.allowProfiling && this.profilerPort) {
injars.push(path.join(this.platformPath, 'lib', 'titanium-profiler.jar'));
injarsAll.push(path.join(this.platformPath, 'lib', 'titanium-profiler.jar'));
}

// nuke and create the folder holding all the classes*.dex files
Expand All @@ -4157,32 +4180,43 @@ AndroidBuilder.prototype.runDexer = function runDexer(next) {
// Wipe existing outjar
fs.existsSync(outjar) && fs.unlinkSync(outjar);

// We need to hack multidex for APi level < 21 to generate the list of classes that *need* to go into the first dex file
// We skip these intermediate steps if 21+ and eventually just run dexer
// Add all Java classes used/declared by the "Application" derived class to main dex file.
// We only do this if the min Android OS version supported is less than 5.0.
// Note: Android OS versions older than 5.0 (API Level 21) do not natively support multidexed apps.
// So, we have to call Multidex.install() Java method upon app startup for Android 4.x support.
// Since Java runtime attempts to find all classes used by "Application" derived class before
// the app can invoke the Multidex.install() method, we must ensure those classes are in the
// main dex file or else the runtime will fail to link those classes and cause a crash on 4.x.
async.series([
function (done) {
// 'api-level' and 'sdk' properties both seem to hold apiLevel
if (this.androidTargetSDK.sdk >= 21) {
// Skip the below if the min Android OS version supported is 5.0 or higher.
if (this.minSDK >= 21) {
return done();
}

// Run: java
// -jar $this.androidInfo.sdk.proguard
// -injars "${@}"
// -dontwarn -forceprocessing
// -outjars ${tmpOut}
// -libraryjars "${shrinkedAndroidJar}"
// -dontoptimize -dontobfuscate -dontpreverify
// -include "${baserules}"
appc.subprocess.run(this.jdkInfo.executables.java, [
'-jar',
this.androidInfo.sdk.proguard,
'-injars', injars.join(':'),
'-dontwarn', '-forceprocessing',
'-outjars', outjar,
'-libraryjars', shrinkedAndroid,
'-dontoptimize', '-dontobfuscate', '-dontpreverify', '-include',
baserules
// Create a ProGuard config file.
let proguardConfig
= '-dontoptimize\n'
+ '-dontobfuscate\n'
+ '-dontpreverify\n'
+ '-dontwarn **\n'
+ '-libraryjars ' + shrinkedAndroid + '\n';
for (let index = 0; index < injarsCore.length; index++) {
proguardConfig += '-injars ' + injarsCore[index] + '(!META-INF/**)\n';
}
proguardConfig += '-outjars ' + outjar + '\n';
const mainDexProGuardFilePath = path.join(this.buildDir, 'mainDexProGuard.txt');
fs.writeFileSync(mainDexProGuardFilePath, proguardConfig);

// Run ProGuard via Gradle to create a single JAR of all the main Java classes used by the app.
// Note: ProGuard included with the Android SDK is very old (v4.x) and doesn't support loading Java 8 JARs,
// such as the JARs Google provides with Android build-tools v27. Google now acquires the newest
// version of ProGuard via Gradle/Maven, which is kept up to date by the ProGuard maintainers.
const gradleAppFileName = (process.platform === 'win32') ? 'gradlew.bat' : 'gradlew';
appc.subprocess.run(path.join(this.buildDir, gradleAppFileName), [
'-b', path.join(this.buildDir, 'proguard.gradle'),
'-Pforceprocessing=true',
'-Pconfiguration=' + baserules + pathArraySeparator + mainDexProGuardFilePath
], {}, function (code, out, err) {
if (code) {
this.logger.error(__('Failed to run dexer:'));
Expand All @@ -4196,12 +4230,12 @@ AndroidBuilder.prototype.runDexer = function runDexer(next) {
}.bind(this),
// Run: java -cp $this.androidInfo.sdk.dx com.android.multidex.MainDexListBuilder "$outjar" "$injars"
function (done) {
// 'api-level' and 'sdk' properties both seem to hold apiLevel
if (this.androidTargetSDK.sdk >= 21) {
// Skip the below if the min Android OS version supported is 5.0 or higher.
if (this.minSDK >= 21) {
return done();
}

appc.subprocess.run(this.jdkInfo.executables.java, [ '-cp', this.androidInfo.sdk.dx, 'com.android.multidex.MainDexListBuilder', outjar, injars.join(':') ], {}, function (code, out, err) {
appc.subprocess.run(this.jdkInfo.executables.java, [ '-cp', this.androidInfo.sdk.dx, 'com.android.multidex.MainDexListBuilder', outjar, injarsCore.join(pathArraySeparator) ], {}, function (code, out, err) {
var mainDexClassesList = path.join(this.buildDir, 'main-dex-classes.txt');
if (code) {
this.logger.error(__('Failed to run dexer:'));
Expand All @@ -4220,7 +4254,7 @@ AndroidBuilder.prototype.runDexer = function runDexer(next) {
}.bind(this));
}.bind(this),
function (done) {
dexArgs = dexArgs.concat(injars);
dexArgs = dexArgs.concat(injarsAll);
dexerHook(this.jdkInfo.executables.java, dexArgs, {}, done);
}.bind(this)
], next);
Expand Down
77 changes: 70 additions & 7 deletions android/templates/build/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@

import org.appcelerator.kroll.runtime.v8.V8Runtime;

import org.appcelerator.kroll.KrollExternalModule;
import org.appcelerator.kroll.KrollModule;
import org.appcelerator.kroll.KrollModuleInfo;
import org.appcelerator.kroll.KrollRuntime;
import org.appcelerator.kroll.util.KrollAssetHelper;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiRootActivity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -33,16 +36,42 @@ public void onCreate()
postAppInfo();

<% if (encryptJS) { %>
KrollAssetHelper.setAssetCrypt(new AssetCryptImpl());
KrollAssetHelper.setAssetCrypt(new AssetCryptImpl());
<% } %>

V8Runtime runtime = new V8Runtime();

<% customModules.forEach(function (module) { %>
runtime.addExternalModule("<%- module.manifest.moduleid %>", <%- module.manifest.moduleid %>.<%- module.apiName %>Bootstrap.class);
<% if (module.isNativeJsModule) { %>
runtime.addExternalCommonJsModule("<%- module.manifest.moduleid %>", <%- module.manifest.moduleid %>.CommonJsSourceProvider.class);
<% } %>
{
String className = "<%- module.manifest.moduleid %>.<%- module.apiName %>Bootstrap";
try {
runtime.addExternalModule(
"<%- module.manifest.moduleid %>",
(Class<KrollExternalModule>) Class.forName(className));
} catch (Throwable ex) {
Log.e(TAG, "Failed to add external module: " + className);
if ((ex instanceof RuntimeException) == false) {
ex = new RuntimeException(ex);
}
throw (RuntimeException) ex;
}
}
<% if (module.isNativeJsModule) { %>
{
String className = "<%- module.manifest.moduleid %>.CommonJsSourceProvider";
try {
runtime.addExternalCommonJsModule(
"<%- module.manifest.moduleid %>",
(Class<KrollExternalModule>) Class.forName(className));
} catch (Throwable ex) {
Log.e(TAG, "Failed to add external CommonJS module: " + className);
if ((ex instanceof RuntimeException) == false) {
ex = new RuntimeException(ex);
}
throw (RuntimeException) ex;
}
}
<% } %>
<% }); %>

KrollRuntime.init(this, runtime);
Expand All @@ -51,7 +80,24 @@ public void onCreate()

<% appModules.forEach(function (module) { %>
<% if (module['on_app_create']) { %>
<%- module['class_name'] %>.<%- module['on_app_create'] %>(this);
{
String className = "<%- module['class_name'] %>";
String methodName = "<%- module['on_app_create'] %>";
try {
Class moduleClass = Class.forName(className);
Method moduleMethod = moduleClass.getMethod(methodName, TiApplication.class);
moduleMethod.invoke(null, this);
} catch (Throwable ex) {
Log.e(TAG, "Error invoking: " + className + "." + methodName + "()");
if ((ex instanceof InvocationTargetException) && (ex.getCause() != null)) {
ex = ex.getCause();
}
if ((ex instanceof RuntimeException) == false) {
ex = new RuntimeException(ex);
}
throw (RuntimeException) ex;
}
}
<% } %>
<% }); %>

Expand All @@ -60,7 +106,24 @@ public void onCreate()
KrollModuleInfo moduleInfo;
<% customModules.forEach(function (module) { %>
<% if (module.onAppCreate) { %>
<%- module.className %>.<%- module.onAppCreate %>(this);
{
String className = "<%- module.className %>";
String methodName = "<%- module.onAppCreate %>";
try {
Class moduleClass = Class.forName(className);
Method moduleMethod = moduleClass.getMethod(methodName, TiApplication.class);
moduleMethod.invoke(null, this);
} catch (Throwable ex) {
Log.e(TAG, "Error invoking: " + className + "." + methodName + "()");
if ((ex instanceof InvocationTargetException) && (ex.getCause() != null)) {
ex = ex.getCause();
}
if ((ex instanceof RuntimeException) == false) {
ex = new RuntimeException(ex);
}
throw (RuntimeException) ex;
}
}
<% } %>

moduleInfo = new KrollModuleInfo(
Expand Down
55 changes: 55 additions & 0 deletions android/templates/build/proguard.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2009-2018 by Axway. All Rights Reserved.
* Licensed under the terms of the Apache Public License.
* Please see the LICENSE included with this distribution for details.
*/

import proguard.gradle.ProGuardTask

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'net.sf.proguard:proguard-gradle:5.3.3'
}
}

task proguard(type: proguard.gradle.ProGuardTask) {
// Disable gradle's incremental build support for this task so that it'll always execute.
outputs.upToDateWhen { false }

// Do not continue if not given a 'configuration' property. It's required.
if (!project.hasProperty('configuration')) {
throw new InvalidUserDataException(
'You must set a "configuration" property referencing a Proguard config file.')
}

// Extract file path(s) from given 'configuration' property and pass them over to the ProGuard task.
// Note: Multiple file paths can be provided like how it works with a PATH environment variable.
// On Windows, file paths are separated by a ';' semicolon.
// On Mac/Linux, file paths are separated by a ':' colon.
def isWindows = System.getProperty('os.name').toLowerCase().contains('windows')
def arraySeparator = isWindows ? ';' : ':'
def configFileArray = project.properties.configuration.split(arraySeparator)
for (nextFile in configFileArray) {
// Verify that the given file exists.
if ((new File(nextFile)).exists() == false) {
throw new InvalidUserDataException('Given file not found: ' + nextFile)
}

// Copy the config file path to the ProGuard task.
configuration nextFile
}

// Enable ProGuard's force processing feature if property was set in gradle.
// Note: This feature cannot be enabled in a ProGuard config file.
if (project.hasProperty('forceprocessing') && project.properties.forceprocessing.toBoolean()) {
forceprocessing
}

// The ProGuard task will execute using the above config file(s) at the end of this gradle task.
}

defaultTasks 'proguard'