cordova-plugin-universal-links-fix
Version:
Cordova plugin to add in your application support for Universal Links (iOS 9) and Deep Links (Android). Basically, open application through the link in the browser.
317 lines (261 loc) • 9.06 kB
JavaScript
/**
Class injects plugin preferences into AndroidManifest.xml file.
*/
var path = require('path');
var xmlHelper = require('../xmlHelper.js');
module.exports = {
writePreferences: writePreferences
};
// region Public API
/**
* Inject preferences into AndroidManifest.xml file.
*
* @param {Object} cordovaContext - cordova context object
* @param {Object} pluginPreferences - plugin preferences as JSON object; already parsed
*/
function writePreferences(cordovaContext, pluginPreferences) {
var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
var manifestSource = xmlHelper.readXmlAsJson(pathToManifest);
var cleanManifest;
var updatedManifest;
// remove old intent-filters
cleanManifest = removeOldOptions(manifestSource);
// inject intent-filters based on plugin preferences
updatedManifest = injectOptions(cleanManifest, pluginPreferences);
// save new version of the AndroidManifest
xmlHelper.writeJsonAsXml(updatedManifest, pathToManifest);
}
// endregion
// region Manifest cleanup methods
/**
* Remove old intent-filters from the manifest file.
*
* @param {Object} manifestData - manifest content as JSON object
* @return {Object} manifest data without old intent-filters
*/
function removeOldOptions(manifestData) {
var cleanManifest = manifestData;
var activities = manifestData['manifest']['application'][0]['activity'];
activities.forEach(removeIntentFiltersFromActivity);
cleanManifest['manifest']['application'][0]['activity'] = activities;
return cleanManifest;
}
/**
* Remove old intent filters from the given activity.
*
* @param {Object} activity - activity, from which we need to remove intent-filters.
* Changes applied to the passed object.
*/
function removeIntentFiltersFromActivity(activity) {
var oldIntentFilters = activity['intent-filter'];
var newIntentFilters = [];
if (oldIntentFilters == null || oldIntentFilters.length == 0) {
return;
}
oldIntentFilters.forEach(function(intentFilter) {
if (!isIntentFilterForUniversalLinks(intentFilter)) {
newIntentFilters.push(intentFilter);
}
});
activity['intent-filter'] = newIntentFilters;
}
/**
* Check if given intent-filter is for Universal Links.
*
* @param {Object} intentFilter - intent-filter to check
* @return {Boolean} true - if intent-filter for Universal Links; otherwise - false;
*/
function isIntentFilterForUniversalLinks(intentFilter) {
var actions = intentFilter['action'];
var categories = intentFilter['category'];
var data = intentFilter['data'];
return isActionForUniversalLinks(actions) &&
isCategoriesForUniversalLinks(categories) &&
isDataTagForUniversalLinks(data);
}
/**
* Check if actions from the intent-filter corresponds to actions for Universal Links.
*
* @param {Array} actions - list of actions in the intent-filter
* @return {Boolean} true - if action for Universal Links; otherwise - false
*/
function isActionForUniversalLinks(actions) {
// there can be only 1 action
if (actions == null || actions.length != 1) {
return false;
}
var action = actions[0]['$']['android:name'];
return ('android.intent.action.VIEW' === action);
}
/**
* Check if categories in the intent-filter corresponds to categories for Universal Links.
*
* @param {Array} categories - list of categories in the intent-filter
* @return {Boolean} true - if action for Universal Links; otherwise - false
*/
function isCategoriesForUniversalLinks(categories) {
// there can be only 2 categories
if (categories == null || categories.length != 2) {
return false;
}
var isBrowsable = false;
var isDefault = false;
// check intent categories
categories.forEach(function(category) {
var categoryName = category['$']['android:name'];
if (!isBrowsable) {
isBrowsable = 'android.intent.category.BROWSABLE' === categoryName;
}
if (!isDefault) {
isDefault = 'android.intent.category.DEFAULT' === categoryName;
}
});
return isDefault && isBrowsable;
}
/**
* Check if data tag from intent-filter corresponds to data for Universal Links.
*
* @param {Array} data - list of data tags in the intent-filter
* @return {Boolean} true - if data tag for Universal Links; otherwise - false
*/
function isDataTagForUniversalLinks(data) {
// can have only 1 data tag in the intent-filter
if (data == null || data.length != 1) {
return false;
}
var dataHost = data[0]['$']['android:host'];
var dataScheme = data[0]['$']['android:scheme'];
var hostIsSet = dataHost != null && dataHost.length > 0;
var schemeIsSet = dataScheme != null && dataScheme.length > 0;
return hostIsSet && schemeIsSet;
}
// endregion
// region Methods to inject preferences into AndroidManifest.xml file
/**
* Inject options into manifest file.
*
* @param {Object} manifestData - manifest content where preferences should be injected
* @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed
* @return {Object} updated manifest data with corresponding intent-filters
*/
function injectOptions(manifestData, pluginPreferences) {
var changedManifest = manifestData;
var activitiesList = changedManifest['manifest']['application'][0]['activity'];
var launchActivityIndex = getMainLaunchActivityIndex(activitiesList);
var ulIntentFilters = [];
var launchActivity;
if (launchActivityIndex < 0) {
console.warn('Could not find launch activity in the AndroidManifest file. Can\'t inject Universal Links preferences.');
return;
}
// get launch activity
launchActivity = activitiesList[launchActivityIndex];
// generate intent-filters
pluginPreferences.hosts.forEach(function(host) {
host.paths.forEach(function(hostPath) {
ulIntentFilters.push(createIntentFilter(host.name, host.scheme, hostPath));
});
});
// add Universal Links intent-filters to the launch activity
launchActivity['intent-filter'] = launchActivity['intent-filter'].concat(ulIntentFilters);
return changedManifest;
}
/**
* Find index of the applications launcher activity.
*
* @param {Array} activities - list of all activities in the app
* @return {Integer} index of the launch activity; -1 - if none was found
*/
function getMainLaunchActivityIndex(activities) {
var launchActivityIndex = -1;
activities.some(function(activity, index) {
if (isLaunchActivity(activity)) {
launchActivityIndex = index;
return true;
}
return false;
});
return launchActivityIndex;
}
/**
* Check if the given actvity is a launch activity.
*
* @param {Object} activity - activity to check
* @return {Boolean} true - if this is a launch activity; otherwise - false
*/
function isLaunchActivity(activity) {
var intentFilters = activity['intent-filter'];
var isLauncher = false;
if (intentFilters == null || intentFilters.length == 0) {
return false;
}
isLauncher = intentFilters.some(function(intentFilter) {
var action = intentFilter['action'];
var category = intentFilter['category'];
if (action == null || action.length != 1 || category == null || category.length != 1) {
return false;
}
var isMainAction = ('android.intent.action.MAIN' === action[0]['$']['android:name']);
var isLauncherCategory = ('android.intent.category.LAUNCHER' === category[0]['$']['android:name']);
return isMainAction && isLauncherCategory;
});
return isLauncher;
}
/**
* Create JSON object that represent intent-filter for universal link.
*
* @param {String} host - host name
* @param {String} scheme - host scheme
* @param {String} pathName - host path
* @return {Object} intent-filter as a JSON object
*/
function createIntentFilter(host, scheme, pathName) {
var intentFilter = {
'$': {
'android:autoVerify': 'true'
},
'action': [{
'$': {
'android:name': 'android.intent.action.VIEW'
}
}],
'category': [{
'$': {
'android:name': 'android.intent.category.DEFAULT'
}
}, {
'$': {
'android:name': 'android.intent.category.BROWSABLE'
}
}],
'data': [{
'$': {
'android:host': host,
'android:scheme': scheme
}
}]
};
injectPathComponentIntoIntentFilter(intentFilter, pathName);
return intentFilter;
}
/**
* Inject host path into provided intent-filter.
*
* @param {Object} intentFilter - intent-filter object where path component should be injected
* @param {String} pathName - host path to inject
*/
function injectPathComponentIntoIntentFilter(intentFilter, pathName) {
if (pathName == null || pathName === '*') {
return;
}
var attrKey = 'android:path';
if (pathName.indexOf('*') >= 0) {
attrKey = 'android:pathPattern';
pathName = pathName.replace(/\*/g, '.*');
}
if (pathName.indexOf('/') != 0) {
pathName = '/' + pathName;
}
intentFilter['data'][0]['$'][attrKey] = pathName;
}
// endregion