You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

manifestWriter.js 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /**
  2. Class injects plugin preferences into AndroidManifest.xml file.
  3. */
  4. var path = require('path');
  5. var xmlHelper = require('../xmlHelper.js');
  6. module.exports = {
  7. writePreferences: writePreferences
  8. };
  9. // region Public API
  10. /**
  11. * Inject preferences into AndroidManifest.xml file.
  12. *
  13. * @param {Object} cordovaContext - cordova context object
  14. * @param {Object} pluginPreferences - plugin preferences as JSON object; already parsed
  15. */
  16. function writePreferences(cordovaContext, pluginPreferences) {
  17. var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
  18. var manifestSource = xmlHelper.readXmlAsJson(pathToManifest);
  19. var cleanManifest;
  20. var updatedManifest;
  21. // remove old intent-filters
  22. cleanManifest = removeOldOptions(manifestSource);
  23. // inject intent-filters based on plugin preferences
  24. updatedManifest = injectOptions(cleanManifest, pluginPreferences);
  25. // save new version of the AndroidManifest
  26. xmlHelper.writeJsonAsXml(updatedManifest, pathToManifest);
  27. }
  28. // endregion
  29. // region Manifest cleanup methods
  30. /**
  31. * Remove old intent-filters from the manifest file.
  32. *
  33. * @param {Object} manifestData - manifest content as JSON object
  34. * @return {Object} manifest data without old intent-filters
  35. */
  36. function removeOldOptions(manifestData) {
  37. var cleanManifest = manifestData;
  38. var activities = manifestData['manifest']['application'][0]['activity'];
  39. activities.forEach(removeIntentFiltersFromActivity);
  40. cleanManifest['manifest']['application'][0]['activity'] = activities;
  41. return cleanManifest;
  42. }
  43. /**
  44. * Remove old intent filters from the given activity.
  45. *
  46. * @param {Object} activity - activity, from which we need to remove intent-filters.
  47. * Changes applied to the passed object.
  48. */
  49. function removeIntentFiltersFromActivity(activity) {
  50. var oldIntentFilters = activity['intent-filter'];
  51. var newIntentFilters = [];
  52. if (oldIntentFilters == null || oldIntentFilters.length == 0) {
  53. return;
  54. }
  55. oldIntentFilters.forEach(function(intentFilter) {
  56. if (!isIntentFilterForUniversalLinks(intentFilter)) {
  57. newIntentFilters.push(intentFilter);
  58. }
  59. });
  60. activity['intent-filter'] = newIntentFilters;
  61. }
  62. /**
  63. * Check if given intent-filter is for Universal Links.
  64. *
  65. * @param {Object} intentFilter - intent-filter to check
  66. * @return {Boolean} true - if intent-filter for Universal Links; otherwise - false;
  67. */
  68. function isIntentFilterForUniversalLinks(intentFilter) {
  69. var actions = intentFilter['action'];
  70. var categories = intentFilter['category'];
  71. var data = intentFilter['data'];
  72. return isActionForUniversalLinks(actions) &&
  73. isCategoriesForUniversalLinks(categories) &&
  74. isDataTagForUniversalLinks(data);
  75. }
  76. /**
  77. * Check if actions from the intent-filter corresponds to actions for Universal Links.
  78. *
  79. * @param {Array} actions - list of actions in the intent-filter
  80. * @return {Boolean} true - if action for Universal Links; otherwise - false
  81. */
  82. function isActionForUniversalLinks(actions) {
  83. // there can be only 1 action
  84. if (actions == null || actions.length != 1) {
  85. return false;
  86. }
  87. var action = actions[0]['$']['android:name'];
  88. return ('android.intent.action.VIEW' === action);
  89. }
  90. /**
  91. * Check if categories in the intent-filter corresponds to categories for Universal Links.
  92. *
  93. * @param {Array} categories - list of categories in the intent-filter
  94. * @return {Boolean} true - if action for Universal Links; otherwise - false
  95. */
  96. function isCategoriesForUniversalLinks(categories) {
  97. // there can be only 2 categories
  98. if (categories == null || categories.length != 2) {
  99. return false;
  100. }
  101. var isBrowsable = false;
  102. var isDefault = false;
  103. // check intent categories
  104. categories.forEach(function(category) {
  105. var categoryName = category['$']['android:name'];
  106. if (!isBrowsable) {
  107. isBrowsable = 'android.intent.category.BROWSABLE' === categoryName;
  108. }
  109. if (!isDefault) {
  110. isDefault = 'android.intent.category.DEFAULT' === categoryName;
  111. }
  112. });
  113. return isDefault && isBrowsable;
  114. }
  115. /**
  116. * Check if data tag from intent-filter corresponds to data for Universal Links.
  117. *
  118. * @param {Array} data - list of data tags in the intent-filter
  119. * @return {Boolean} true - if data tag for Universal Links; otherwise - false
  120. */
  121. function isDataTagForUniversalLinks(data) {
  122. // can have only 1 data tag in the intent-filter
  123. if (data == null || data.length != 1) {
  124. return false;
  125. }
  126. var dataHost = data[0]['$']['android:host'];
  127. var dataScheme = data[0]['$']['android:scheme'];
  128. var hostIsSet = dataHost != null && dataHost.length > 0;
  129. var schemeIsSet = dataScheme != null && dataScheme.length > 0;
  130. return hostIsSet && schemeIsSet;
  131. }
  132. // endregion
  133. // region Methods to inject preferences into AndroidManifest.xml file
  134. /**
  135. * Inject options into manifest file.
  136. *
  137. * @param {Object} manifestData - manifest content where preferences should be injected
  138. * @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed
  139. * @return {Object} updated manifest data with corresponding intent-filters
  140. */
  141. function injectOptions(manifestData, pluginPreferences) {
  142. var changedManifest = manifestData;
  143. var activitiesList = changedManifest['manifest']['application'][0]['activity'];
  144. var launchActivityIndex = getMainLaunchActivityIndex(activitiesList);
  145. var ulIntentFilters = [];
  146. var launchActivity;
  147. if (launchActivityIndex < 0) {
  148. console.warn('Could not find launch activity in the AndroidManifest file. Can\'t inject Universal Links preferences.');
  149. return;
  150. }
  151. // get launch activity
  152. launchActivity = activitiesList[launchActivityIndex];
  153. // generate intent-filters
  154. pluginPreferences.hosts.forEach(function(host) {
  155. host.paths.forEach(function(hostPath) {
  156. ulIntentFilters.push(createIntentFilter(host.name, host.scheme, hostPath));
  157. });
  158. });
  159. // add Universal Links intent-filters to the launch activity
  160. launchActivity['intent-filter'] = launchActivity['intent-filter'].concat(ulIntentFilters);
  161. return changedManifest;
  162. }
  163. /**
  164. * Find index of the applications launcher activity.
  165. *
  166. * @param {Array} activities - list of all activities in the app
  167. * @return {Integer} index of the launch activity; -1 - if none was found
  168. */
  169. function getMainLaunchActivityIndex(activities) {
  170. var launchActivityIndex = -1;
  171. activities.some(function(activity, index) {
  172. if (isLaunchActivity(activity)) {
  173. launchActivityIndex = index;
  174. return true;
  175. }
  176. return false;
  177. });
  178. return launchActivityIndex;
  179. }
  180. /**
  181. * Check if the given actvity is a launch activity.
  182. *
  183. * @param {Object} activity - activity to check
  184. * @return {Boolean} true - if this is a launch activity; otherwise - false
  185. */
  186. function isLaunchActivity(activity) {
  187. var intentFilters = activity['intent-filter'];
  188. var isLauncher = false;
  189. if (intentFilters == null || intentFilters.length == 0) {
  190. return false;
  191. }
  192. isLauncher = intentFilters.some(function(intentFilter) {
  193. var action = intentFilter['action'];
  194. var category = intentFilter['category'];
  195. if (action == null || action.length != 1 || category == null || category.length != 1) {
  196. return false;
  197. }
  198. var isMainAction = ('android.intent.action.MAIN' === action[0]['$']['android:name']);
  199. var isLauncherCategory = ('android.intent.category.LAUNCHER' === category[0]['$']['android:name']);
  200. return isMainAction && isLauncherCategory;
  201. });
  202. return isLauncher;
  203. }
  204. /**
  205. * Create JSON object that represent intent-filter for universal link.
  206. *
  207. * @param {String} host - host name
  208. * @param {String} scheme - host scheme
  209. * @param {String} pathName - host path
  210. * @return {Object} intent-filter as a JSON object
  211. */
  212. function createIntentFilter(host, scheme, pathName) {
  213. var intentFilter = {
  214. '$': {
  215. 'android:autoVerify': 'true'
  216. },
  217. 'action': [{
  218. '$': {
  219. 'android:name': 'android.intent.action.VIEW'
  220. }
  221. }],
  222. 'category': [{
  223. '$': {
  224. 'android:name': 'android.intent.category.DEFAULT'
  225. }
  226. }, {
  227. '$': {
  228. 'android:name': 'android.intent.category.BROWSABLE'
  229. }
  230. }],
  231. 'data': [{
  232. '$': {
  233. 'android:host': host,
  234. 'android:scheme': scheme
  235. }
  236. }]
  237. };
  238. injectPathComponentIntoIntentFilter(intentFilter, pathName);
  239. return intentFilter;
  240. }
  241. /**
  242. * Inject host path into provided intent-filter.
  243. *
  244. * @param {Object} intentFilter - intent-filter object where path component should be injected
  245. * @param {String} pathName - host path to inject
  246. */
  247. function injectPathComponentIntoIntentFilter(intentFilter, pathName) {
  248. if (pathName == null || pathName === '*') {
  249. return;
  250. }
  251. var attrKey = 'android:path';
  252. if (pathName.indexOf('*') >= 0) {
  253. attrKey = 'android:pathPattern';
  254. pathName = pathName.replace(/\*/g, '.*');
  255. }
  256. if (pathName.indexOf('/') != 0) {
  257. pathName = '/' + pathName;
  258. }
  259. intentFilter['data'][0]['$'][attrKey] = pathName;
  260. }
  261. // endregion