Skip to content

Commit

Permalink
Merge pull request #9707 from jquick-axway/TIMOB-25597-7_0_X
Browse files Browse the repository at this point in the history
[7_0_X][TIMOB-25597] Android: Fixed bug where multidexed apps crash on startup on Android 4.x
  • Loading branch information
eric34 committed Jan 24, 2018
2 parents 4c70b76 + fbb1f24 commit 2ac580d
Show file tree
Hide file tree
Showing 8 changed files with 638 additions and 39 deletions.
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'

0 comments on commit 2ac580d

Please sign in to comment.