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

fix(android)(8_0_X): Improved merge of "tiapp.xml" android manifest settings #10900

Merged
merged 3 commits into from
May 21, 2019
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
174 changes: 57 additions & 117 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -1857,7 +1857,6 @@ AndroidBuilder.prototype.initialize = function initialize(next) {
return appc.string.capitalize(word.toLowerCase());
}).join('');
/^[0-9]/.test(this.classname) && (this.classname = '_' + this.classname);
this.mainActivity = '.' + this.classname + 'Activity';

this.buildOnly = argv['build-only'];

Expand Down Expand Up @@ -3722,66 +3721,50 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
'Contacts.getAllGroups': contactsReadPermissions,
'Contacts.getGroupByID': contactsReadPermissions,

'Map.createView': geoPermissions,

'Media.Android.setSystemWallpaper': wallpaperPermissions,
'Media.showCamera': cameraPermissions,
'Media.vibrate': vibratePermissions,
},

tiMethodActivities = {
'Map.createView': {
activity: {
name: 'ti.modules.titanium.map.TiMapActivity',
configChanges: [ 'keyboardHidden', 'orientation' ],
launchMode: 'singleTask'
},
'uses-library': {
name: 'com.google.android.maps'
}
},
'Media.createVideoPlayer': {
activity: {
name: 'ti.modules.titanium.media.TiVideoActivity',
configChanges: [ 'keyboardHidden', 'orientation' ],
theme: '@style/Theme.AppCompat.Fullscreen',
launchMode: 'singleTask'
}
},
'Media.showCamera': {
activity: {
name: 'ti.modules.titanium.media.TiCameraActivity',
configChanges: [ 'keyboardHidden', 'orientation' ],
theme: '@style/Theme.AppCompat.Translucent.NoTitleBar.Fullscreen'
}
}
},

googleAPIs = [
'Map.createView'
// Example: 'Map.createView'
],

enableGoogleAPIWarning = this.target === 'emulator' && this.emulator && !this.emulator.googleApis,

fill = function (str) {
// first we replace all legacy variable placeholders with EJS style placeholders
str = str.replace(/(\$\{tiapp\.properties\[['"]([^'"]+)['"]\]\})/g, function (s, m1, m2) {
// if the property is the "id", we want to force our scrubbed "appid"
if (m2 === 'id') {
m2 = 'appid';
} else {
m2 = 'tiapp.' + m2;
}
return '<%- ' + m2 + ' %>';
});
// then process the string as an EJS template
return ejs.render(str, this);
}.bind(this),

finalAndroidManifest = (new AndroidManifest()).parse(fill(fs.readFileSync(path.join(this.templatesDir, 'AndroidManifest.xml')).toString())),
customAndroidManifest = this.customAndroidManifest,
tiappAndroidManifest = this.tiappAndroidManifest;

// Create a string of all known <activity/> attribute "android:configChanges" values for the target API Level.
// This variable will be referenced by name by an EJS "AndroidManifest.xml" template.
// Ex: <activity android:name="MyActivity" android:configChanges="<%- allActivityConfigChanges %>" />
this.allActivityConfigChanges
= 'fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation'
+ '|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode';
if (this.realTargetSDK >= 24) {
this.allActivityConfigChanges += '|density';
}

// Function used to EJS render Titanium's main "AndroidManfiest.xml" template and "timodule.xml" templates.
const ejsRenderManifest = function (str) {
// first we replace all legacy variable placeholders with EJS style placeholders
str = str.replace(/(\$\{tiapp\.properties\[['"]([^'"]+)['"]\]\})/g, function (s, m1, m2) {
// if the property is the "id", we want to force our scrubbed "appid"
if (m2 === 'id') {
m2 = 'appid';
} else {
m2 = 'tiapp.' + m2;
}
return '<%- ' + m2 + ' %>';
});
// then process the string as an EJS template
return ejs.render(str, this);
}.bind(this);

// Fetch main Titanium "AndroidManifest.xml" template's settings, resolving all template variables via EJS.
const finalAndroidManifest = (new AndroidManifest()).parse(ejsRenderManifest(
fs.readFileSync(path.join(this.templatesDir, 'AndroidManifest.xml')).toString()));

// if they are using a custom AndroidManifest and merging is disabled, then write the custom one as is
if (!this.config.get('android.mergeCustomAndroidManifest', true) && this.customAndroidManifest) {
(this.cli.createHook('build.android.writeAndroidManifest', this, function (file, xml, done) {
Expand Down Expand Up @@ -3823,18 +3806,6 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
});
}

const obj = tiMethodActivities[symbol];
if (obj) {
if (obj.activity) {
finalAndroidManifest.application.activity || (finalAndroidManifest.application.activity = {});
finalAndroidManifest.application.activity[obj.activity.name] = obj.activity;
}
if (obj['uses-library']) {
finalAndroidManifest.application['uses-library'] || (finalAndroidManifest.application['uses-library'] = {});
finalAndroidManifest.application['uses-library'][obj['uses-library'].name] = obj['uses-library'];
}
}

if (enableGoogleAPIWarning && googleAPIs.indexOf(symbol) !== -1) {
const fn = 'Titanium.' + symbol + '()';
if (this.emulator.googleApis === null) {
Expand All @@ -3859,14 +3830,6 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
this.logger.warn(__('Setting "%s" is not recommended for activity "%s"', 'android:launchMode'.red, activity.cyan));
}
}

// TIMOB-24917: make sure the main activity is themed
if (tiappAndroidManifest.application.activity && tiappAndroidManifest.application.activity[this.mainActivity]) {
const parameters = tiappAndroidManifest.application.activity[this.mainActivity];
if (!parameters.theme) {
parameters.theme = '@style/Theme.Titanium';
}
}
}

// gather activities
Expand All @@ -3882,7 +3845,6 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
a[key.replace(/^android:/, '')] = activity[key];
}
});
a.configChanges || (a.configChanges = [ 'keyboardHidden', 'orientation' ]);
finalAndroidManifest.application.activity || (finalAndroidManifest.application.activity = {});
finalAndroidManifest.application.activity[a.name] = a;
}
Expand Down Expand Up @@ -3932,19 +3894,35 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
// set the app icon
finalAndroidManifest.application.icon = '@drawable/' + this.tiapp.icon.replace(/((\.9)?\.(png|jpg))$/, '');

// merge the custom android manifest
finalAndroidManifest.merge(customAndroidManifest);
// Merge in "AndroidManifest.xml" files belonging to all AAR libraries.
this.androidLibraries.forEach(libraryInfo => {
const libraryManifestPath = path.join(libraryInfo.explodedPath, 'AndroidManifest.xml');
if (fs.existsSync(libraryManifestPath)) {
let libraryManifestContent = fs.readFileSync(libraryManifestPath).toString();

// merge the tiapp.xml android manifest
finalAndroidManifest.merge(tiappAndroidManifest);
// handle injected build variables such as ${applicationId}
// https://developer.android.com/studio/build/manifest-build-variables
libraryManifestContent = libraryManifestContent.replace(/\$\{applicationId\}/g, this.appid); // eslint-disable-line no-template-curly-in-string

const libraryManifest = new AndroidManifest();
libraryManifest.parse(libraryManifestContent);

// we don't want android libraries to override the <supports-screens> or <uses-sdk> tags
delete libraryManifest.__attr__;
delete libraryManifest['supports-screens'];
delete libraryManifest['uses-sdk'];
finalAndroidManifest.merge(libraryManifest);
}
});

// Merge in "AndroidManifest.xml" settings embedded within all "timodule.xml" files.
this.modules.forEach(function (module) {
const moduleXmlFile = path.join(module.modulePath, 'timodule.xml');
if (fs.existsSync(moduleXmlFile)) {
const moduleXml = new tiappxml(moduleXmlFile);
if (moduleXml.android && moduleXml.android.manifest) {
const am = new AndroidManifest();
am.parse(fill(moduleXml.android.manifest));
am.parse(ejsRenderManifest(moduleXml.android.manifest));
// we don't want modules to override the <supports-screens> or <uses-sdk> tags
delete am.__attr__;
delete am['supports-screens'];
Expand All @@ -3959,55 +3937,17 @@ AndroidBuilder.prototype.generateAndroidManifest = function generateAndroidManif
}
}, this);

this.androidLibraries.forEach(libraryInfo => {
const libraryManifestPath = path.join(libraryInfo.explodedPath, 'AndroidManifest.xml');
if (fs.existsSync(libraryManifestPath)) {
let libraryManifestContent = fs.readFileSync(libraryManifestPath).toString();

// handle injected build variables such as ${applicationId}
// https://developer.android.com/studio/build/manifest-build-variables
libraryManifestContent = libraryManifestContent.replace(/\$\{applicationId\}/g, this.appid); // eslint-disable-line no-template-curly-in-string

const libraryManifest = new AndroidManifest();
libraryManifest.parse(libraryManifestContent);

// we don't want android libraries to override the <supports-screens> or <uses-sdk> tags
delete libraryManifest.__attr__;
delete libraryManifest['supports-screens'];
delete libraryManifest['uses-sdk'];
finalAndroidManifest.merge(libraryManifest);
}
});
// Merge in the custom android manifest file.
finalAndroidManifest.merge(customAndroidManifest);

// if the target sdk is Android 3.2 or newer, then we need to add 'screenSize' to
// the default AndroidManifest.xml's 'configChanges' attribute for all <activity>
// elements, otherwise changes in orientation will cause the app to restart
if (this.realTargetSDK >= 13) {
Object.keys(finalAndroidManifest.application.activity).forEach(function (name) {
const activity = finalAndroidManifest.application.activity[name];
if (!activity.configChanges) {
activity.configChanges = [ 'screenSize' ];
} else if (activity.configChanges.indexOf('screenSize') === -1) {
activity.configChanges.push('screenSize');
}
});
}
// Merge in the "tiapp.xml" file's android manifest settings.
// Must be done last so app developer can override manifest settings such as <activity/>, <receiver/>, etc.
finalAndroidManifest.merge(tiappAndroidManifest);

if (this.realTargetSDK >= 24 && !finalAndroidManifest.application.hasOwnProperty('resizeableActivity')) {
finalAndroidManifest.application.resizeableActivity = true;
}

if (this.realTargetSDK >= 24) {
Object.keys(finalAndroidManifest.application.activity).forEach(function (name) {
const activity = finalAndroidManifest.application.activity[name];
if (!activity.configChanges) {
activity.configChanges = [ 'density' ];
} else if (activity.configChanges.indexOf('density') === -1) {
activity.configChanges.push('density');
}
});
}

// add permissions
if (!this.tiapp['override-permissions']) {
Array.isArray(finalAndroidManifest['uses-permission']) || (finalAndroidManifest['uses-permission'] = []);
Expand Down
43 changes: 36 additions & 7 deletions android/cli/lib/AndroidManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ const appc = require('node-appc'),
},

tagAttrs = {
application: /^(allowTaskReparenting|allowBackup|backupAgent|backupInForeground|banner|debuggable|description|directBootAware|enabled|extractNativeLibs|fullBackupContent|fullBackupOnly|hasCode|hardwareAccelerated|icon|roundIcon|isGame|killAfterRestore|largeHeap|label|logo|manageSpaceActivity|name|networkSecurityConfig|permission|persistent|process|restoreAnyVersion|requiredAccountType|resizeableActivity|restrictedAccountType|supportsRtl|taskAffinity|testOnly|theme|uiOptions|usesCleartextTraffic|vmSafeMode)$/, // eslint-disable-line max-len
activity: /^(allowEmbedded|allowTaskReparenting|alwaysRetainTaskState|autoRemoveFromRecents|banner|clearTaskOnLaunch|configChanges|density|directBootAware|documentLaunchMode|enabled|excludeFromRecents|exported|finishOnTaskLaunch|hardwareAccelerated|icon|label|launchMode|maxRecents|multiprocess|name|noHistory|parentActivityName|permission|persistableMode|process|relinquishTaskIdentity|resizeableActivity|screenOrientation|showForAllUsers|stateNotNeeded|supportsPictureInPicture|taskAffinity|theme|uiOptions|windowSoftInputMode)$/, // eslint-disable-line max-len
application: /^(allowTaskReparenting|allowBackup|allowClearUserData|backupAgent|backupInForeground|banner|debuggable|description|directBootAware|enabled|extractNativeLibs|fullBackupContent|fullBackupOnly|hasCode|hardwareAccelerated|icon|isGame|killAfterRestore|largeHeap|label|logo|manageSpaceActivity|name|networkSecurityConfig|permission|persistent|process|restoreAnyVersion|requiredAccountType|resizeableActivity|restrictedAccountType|roundIcon|supportsRtl|taskAffinity|testOnly|theme|uiOptions|usesCleartextTraffic|vmSafeMode)$/, // eslint-disable-line max-len
activity: /^(allowEmbedded|allowTaskReparenting|alwaysRetainTaskState|autoRemoveFromRecents|banner|clearTaskOnLaunch|colorMode|configChanges|density|directBootAware|documentLaunchMode|enabled|excludeFromRecents|exported|finishOnTaskLaunch|hardwareAccelerated|icon|immersive|label|launchMode|lockTaskMode|maxRecents|maxAspectRatio|multiprocess|name|noHistory|parentActivityName|permission|persistableMode|process|relinquishTaskIdentity|resizeableActivity|screenOrientation|showForAllUsers|stateNotNeeded|supportsPictureInPicture|taskAffinity|theme|uiOptions|windowSoftInputMode)$/, // eslint-disable-line max-len
'activity-alias': /^(enabled|exported|icon|label|name|permission|targetActivity)$/,
data: /^(host|mimeType|path|pathPattern|pathPrefix|port|scheme)$/,
'intent-filter': /^(icon|label|priority)$/,
'meta-data': /^(name|resource|value)$/,
'path-permission': /^(path|pathPrefix|pathPattern|permission|readPermissions|writePermissions)$/,
provider: /^(authorities|enabled|exported|grantUriPermissions|icon|initOrder|label|multiprocess|name|permission|process|readPermission|syncable|writePermission)$/,
receiver: /^(enabled|exported|icon|label|name|permission|process)$/,
service: /^(enabled|exported|icon|isolatedProcess|label|name|permission|process)$/,
provider: /^(authorities|directBootAware|enabled|exported|grantUriPermissions|icon|initOrder|label|multiprocess|name|permission|process|readPermission|syncable|writePermission)$/,
receiver: /^(directBootAware|enabled|exported|icon|label|name|permission|process)$/,
service: /^(description|directBootAware|enabled|exported|icon|isolatedProcess|label|name|permission|process)$/,
'uses-library': /^(name|required)$/,
'uses-sdk': /^(name|required)$/
'uses-sdk': /^(maxSdkVersion|minSdkVersion|targetSdkVersion)$/
};

module.exports = AndroidManifest;
Expand Down Expand Up @@ -456,7 +456,36 @@ function AndroidManifest(filename) {
case 'uses-library':
this[tag][subtag] || (this[tag][subtag] = {});
Object.keys(src[tag][subtag]).forEach(function (key) {
this[tag][subtag][key] = src[tag][subtag][key];
// Copy source's XML tag attributes and child tags to target.
const nextSource = src[tag][subtag][key];
const nextTarget = this[tag][subtag][key];
if (!nextTarget) {
// Insert source's attribute or sub tag to target.
this[tag][subtag][key] = nextSource;
} else {
Object.keys(nextSource).forEach(function (subKey) {
if (subKey === 'configChanges') {
// For <activity/> "android:configChanges" attribute, only copy
// the attribute values from source that are missing in target.
const sourceArray = nextSource[subKey];
if (Array.isArray(sourceArray)) {
const targetArray = nextTarget[subKey];
if (Array.isArray(targetArray)) {
for (let nextValue of sourceArray) {
if (!targetArray.includes(nextValue)) {
targetArray.push(nextValue);
}
}
} else {
nextSource[subKey] = sourceArray;
}
}
} else {
// Overwrite target's attribute or sub tag with source.
nextTarget[subKey] = nextSource[subKey];
}
});
}
}, this);
break;
default:
Expand Down
25 changes: 18 additions & 7 deletions android/templates/build/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,31 @@
android:theme="@style/Theme.AppCompat<% if (tiapp.fullscreen || tiapp['statusbar-hidden'] || tiapp['navbar-hidden']) { %>.NoTitleBar<% if (tiapp.fullscreen || tiapp['statusbar-hidden']) { %>.Fullscreen<% } %><% } %>">

<activity android:name=".<%- classname %>Activity"
android:label="@string/app_name" android:theme="@style/Theme.Titanium"
android:alwaysRetainTaskState="true"
android:configChanges="keyboardHidden|orientation|fontScale|screenSize|smallestScreenSize|screenLayout">
android:configChanges="<%- allActivityConfigChanges %>"
android:label="@string/app_name"
android:theme="@style/Theme.Titanium"
android:alwaysRetainTaskState="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="org.appcelerator.titanium.TiActivity"
android:configChanges="keyboardHidden|orientation|fontScale|screenSize|smallestScreenSize|screenLayout" />
<activity android:name="org.appcelerator.titanium.TiTranslucentActivity"
android:configChanges="keyboardHidden|orientation|fontScale|screenSize|smallestScreenSize|screenLayout"
<activity
android:name="org.appcelerator.titanium.TiActivity"
android:configChanges="<%- allActivityConfigChanges %>" />
<activity
android:name="org.appcelerator.titanium.TiTranslucentActivity"
android:configChanges="<%- allActivityConfigChanges %>"
android:theme="@style/Theme.Titanium.Translucent" />
<activity
android:name="ti.modules.titanium.media.TiCameraActivity"
android:configChanges="<%- allActivityConfigChanges %>"
android:theme="@style/Theme.AppCompat.Translucent.NoTitleBar.Fullscreen" />
<activity
android:name="ti.modules.titanium.media.TiVideoActivity"
android:configChanges="<%- allActivityConfigChanges %>"
android:theme="@style/Theme.AppCompat.Fullscreen" />
<activity android:name="ti.modules.titanium.ui.android.TiPreferencesActivity" />

<provider
Expand Down