generate_xml_from_google_services_json.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. #!/usr/bin/python
  2. # Copyright 2016 Google LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Stand-alone implementation of the Gradle Firebase plugin.
  16. Converts the services json file to xml:
  17. https://googleplex-android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/google-services/src/main/groovy/com/google/gms/googleservices
  18. """
  19. __author__ = 'Wouter van Oortmerssen'
  20. import argparse
  21. import ctypes
  22. import json
  23. import os
  24. import platform
  25. import sys
  26. from xml.etree import ElementTree
  27. # Input filename if it isn't set.
  28. DEFAULT_INPUT_FILENAME = 'app/google-services.json'
  29. # Output filename if it isn't set.
  30. DEFAULT_OUTPUT_FILENAME = 'res/values/googleservices.xml'
  31. # Input filename for .plist files, if it isn't set.
  32. DEFAULT_PLIST_INPUT_FILENAME = 'GoogleService-Info.plist'
  33. # Output filename for .json files, if it isn't set.
  34. DEFAULT_JSON_OUTPUT_FILENAME = 'google-services-desktop.json'
  35. # Indicates a web client in the oauth_client list.
  36. OAUTH_CLIENT_TYPE_WEB = 3
  37. def read_xml_value(xml_node):
  38. """Utility method for reading values from the plist XML.
  39. Args:
  40. xml_node: An ElementTree node, that contains a value.
  41. Returns:
  42. The value of the node, or None, if it could not be read.
  43. """
  44. if xml_node.tag == 'string':
  45. return xml_node.text
  46. elif xml_node.tag == 'integer':
  47. return int(xml_node.text)
  48. elif xml_node.tag == 'real':
  49. return float(xml_node.text)
  50. elif xml_node.tag == 'false':
  51. return 0
  52. elif xml_node.tag == 'true':
  53. return 1
  54. else:
  55. # other types of input are ignored. (data, dates, arrays, etc.)
  56. return None
  57. def construct_plist_dictionary(xml_root):
  58. """Constructs a dictionary of values based on the contents of a plist file.
  59. Args:
  60. xml_root: An ElementTree node, that represents the root of the xml file
  61. that is to be parsed. (Which should be a dictionary containing
  62. key-value pairs of the properties that need to be extracted.)
  63. Returns:
  64. A dictionary, containing key-value pairs for all (supported) entries in the
  65. node.
  66. """
  67. xml_dict = xml_root.find('dict')
  68. if xml_dict is None:
  69. return None
  70. plist_dict = {}
  71. i = 0
  72. while i < len(xml_dict):
  73. if xml_dict[i].tag == 'key':
  74. key = xml_dict[i].text
  75. i += 1
  76. if i < len(xml_dict):
  77. value = read_xml_value(xml_dict[i])
  78. if value is not None:
  79. plist_dict[key] = value
  80. i += 1
  81. return plist_dict
  82. def construct_google_services_json(xml_dict):
  83. """Constructs a google services json file from a dictionary.
  84. Args:
  85. xml_dict: A dictionary of all the key/value pairs that are needed for the
  86. output json file.
  87. Returns:
  88. A string representing the output json file.
  89. """
  90. try:
  91. json_struct = {
  92. 'project_info': {
  93. 'project_number': xml_dict['GCM_SENDER_ID'],
  94. 'firebase_url': xml_dict['DATABASE_URL'],
  95. 'project_id': xml_dict['PROJECT_ID'],
  96. 'storage_bucket': xml_dict['STORAGE_BUCKET']
  97. },
  98. 'client': [{
  99. 'client_info': {
  100. 'mobilesdk_app_id': xml_dict['GOOGLE_APP_ID'],
  101. 'android_client_info': {
  102. 'package_name': xml_dict['BUNDLE_ID']
  103. }
  104. },
  105. 'oauth_client': [{
  106. 'client_id': xml_dict['CLIENT_ID'],
  107. }],
  108. 'api_key': [{
  109. 'current_key': xml_dict['API_KEY']
  110. }],
  111. 'services': {
  112. 'analytics_service': {
  113. 'status': xml_dict['IS_ANALYTICS_ENABLED']
  114. },
  115. 'appinvite_service': {
  116. 'status': xml_dict['IS_APPINVITE_ENABLED']
  117. }
  118. }
  119. },],
  120. 'configuration_version':
  121. '1'
  122. }
  123. return json.dumps(json_struct, indent=2)
  124. except KeyError as e:
  125. sys.stderr.write('Could not find key in plist file: [%s]\n' % (e.args[0]))
  126. return None
  127. def convert_plist_to_json(plist_string, input_filename):
  128. """Converts an input plist string into a .json file and saves it.
  129. Args:
  130. plist_string: The contents of the loaded plist file.
  131. input_filename: The file name that the plist data was read from.
  132. Returns:
  133. the converted string, or None if there were errors.
  134. """
  135. try:
  136. root = ElementTree.fromstring(plist_string)
  137. except ElementTree.ParseError:
  138. sys.stderr.write('Error parsing file %s.\n'
  139. 'It does not appear to be valid XML.\n' % (input_filename))
  140. return None
  141. plist_dict = construct_plist_dictionary(root)
  142. if plist_dict is None:
  143. sys.stderr.write('In file %s, could not locate a top-level \'dict\' '
  144. 'element.\n'
  145. 'File format should be plist XML, with a top-level '
  146. 'dictionary containing project settings as key-value '
  147. 'pairs.\n' % (input_filename))
  148. return None
  149. json_string = construct_google_services_json(plist_dict)
  150. return json_string
  151. def gen_string(parent, name, text):
  152. """Generate one <string /> element and put into the list of keeps.
  153. Args:
  154. parent: The object that will hold the string.
  155. name: The name to store the string under.
  156. text: The text of the string.
  157. """
  158. if text:
  159. prev = parent.get('tools:keep', '')
  160. if prev:
  161. prev += ','
  162. parent.set('tools:keep', prev + '@string/' + name)
  163. child = ElementTree.SubElement(parent, 'string', {
  164. 'name': name,
  165. 'translatable': 'false'
  166. })
  167. child.text = text
  168. def indent(elem, level=0):
  169. """Recurse through XML tree and add indentation.
  170. Args:
  171. elem: The element to recurse over
  172. level: The current indentation level.
  173. """
  174. i = '\n' + level*' '
  175. if elem is not None:
  176. if not elem.text or not elem.text.strip():
  177. elem.text = i + ' '
  178. if not elem.tail or not elem.tail.strip():
  179. elem.tail = i
  180. for elem in elem:
  181. indent(elem, level+1)
  182. if not elem.tail or not elem.tail.strip():
  183. elem.tail = i
  184. else:
  185. if level and (not elem.tail or not elem.tail.strip()):
  186. elem.tail = i
  187. def argv_as_unicode_win32():
  188. """Returns unicode command line arguments on windows.
  189. """
  190. get_command_line_w = ctypes.cdll.kernel32.GetCommandLineW
  191. get_command_line_w.restype = ctypes.wintypes.LPCWSTR
  192. # CommandLineToArgvW parses the Unicode command line
  193. command_line_to_argv_w = ctypes.windll.shell32.CommandLineToArgvW
  194. command_line_to_argv_w.argtypes = [
  195. ctypes.wintypes.LPCWSTR,
  196. ctypes.wintypes.POINTER(ctypes.wintypes.c_int)
  197. ]
  198. command_line_to_argv_w.restype = ctypes.wintypes.POINTER(
  199. ctypes.wintypes.LPWSTR)
  200. argc = ctypes.wintypes.c_int(0)
  201. argv = command_line_to_argv_w(get_command_line_w(), argc)
  202. # Strip the python executable from the arguments if it exists
  203. # (It would be listed as the first argument on the windows command line, but
  204. # not in the arguments to the python script)
  205. sys_argv_len = len(sys.argv)
  206. return [unicode(argv[i]) for i in
  207. range(argc.value - sys_argv_len, argc.value)]
  208. def main():
  209. parser = argparse.ArgumentParser(
  210. description=((
  211. 'Converts a Firebase %s into %s similar to the Gradle plugin, or '
  212. 'converts a Firebase %s into a %s suitible for use on desktop apps.' %
  213. (DEFAULT_INPUT_FILENAME, DEFAULT_OUTPUT_FILENAME,
  214. DEFAULT_PLIST_INPUT_FILENAME, DEFAULT_JSON_OUTPUT_FILENAME))))
  215. parser.add_argument('-i', help='Override input file name',
  216. metavar='FILE', required=False)
  217. parser.add_argument('-o', help='Override destination file name',
  218. metavar='FILE', required=False)
  219. parser.add_argument('-p', help=('Package ID to select within the set of '
  220. 'packages in the input file. If this is '
  221. 'not specified, the first package in the '
  222. 'input file is selected.'))
  223. parser.add_argument('-l', help=('List all package IDs referenced by the '
  224. 'input file. If this is specified, '
  225. 'the output file is not created.'),
  226. action='store_true', default=False, required=False)
  227. parser.add_argument('-f', help=('Print project fields from the input file '
  228. 'in the form \'name=value\\n\' for each '
  229. 'field. If this is specified, the output '
  230. 'is not created.'),
  231. action='store_true', default=False, required=False)
  232. parser.add_argument(
  233. '--plist',
  234. help=(
  235. 'Specifies a plist file to convert to a JSON configuration file. '
  236. 'If this is enabled, the script will expect a .plist file as input, '
  237. 'which it will convert into %s file. The output file is '
  238. '*not* suitable for use with Firebase on Android.' %
  239. (DEFAULT_JSON_OUTPUT_FILENAME)),
  240. action='store_true',
  241. default=False,
  242. required=False)
  243. # python 2 on Windows doesn't handle unicode arguments well, so we need to
  244. # pre-process the command line arguments before trying to parse them.
  245. if platform.system() == 'Windows':
  246. sys.argv = argv_as_unicode_win32()
  247. args = parser.parse_args()
  248. if args.plist:
  249. input_filename = DEFAULT_PLIST_INPUT_FILENAME
  250. output_filename = DEFAULT_JSON_OUTPUT_FILENAME
  251. else:
  252. input_filename = DEFAULT_INPUT_FILENAME
  253. output_filename = DEFAULT_OUTPUT_FILENAME
  254. if args.i:
  255. input_filename_raw = args.i
  256. # Encode the input string (type unicode) as a normal string (type str)
  257. # using the 'utf-8' encoding so that it can be worked with the same as
  258. # input names from other sources (like the defaults).
  259. input_filename = input_filename_raw.encode('utf-8')
  260. if args.o:
  261. output_filename = args.o
  262. # Decode the filename to a unicode string using the 'utf-8' encoding to
  263. # properly handle filepaths with unicode characters in them.
  264. with open(input_filename.decode('utf-8'), 'r') as ifile:
  265. file_string = ifile.read()
  266. json_string = None
  267. if args.plist:
  268. json_string = convert_plist_to_json(file_string, input_filename)
  269. if json_string is None:
  270. return 1
  271. jsobj = json.loads(json_string)
  272. else:
  273. jsobj = json.loads(file_string)
  274. root = ElementTree.Element('resources')
  275. root.set('xmlns:tools', 'http://schemas.android.com/tools')
  276. project_info = jsobj.get('project_info')
  277. if project_info:
  278. gen_string(root, 'firebase_database_url', project_info.get('firebase_url'))
  279. gen_string(root, 'gcm_defaultSenderId', project_info.get('project_number'))
  280. gen_string(root, 'google_storage_bucket',
  281. project_info.get('storage_bucket'))
  282. gen_string(root, 'project_id', project_info.get('project_id'))
  283. if args.f:
  284. if not project_info:
  285. sys.stderr.write('No project info found in %s.' % input_filename)
  286. return 1
  287. for field, value in project_info.iteritems():
  288. sys.stdout.write('%s=%s\n' % (field, value))
  289. return 0
  290. packages = set()
  291. client_list = jsobj.get('client')
  292. if client_list:
  293. # Search for the user specified package in the file.
  294. selected_package_name = ''
  295. selected_client = client_list[0]
  296. find_package_name = args.p
  297. for client in client_list:
  298. package_name = client.get('client_info', {}).get(
  299. 'android_client_info', {}).get('package_name', '')
  300. if not package_name:
  301. package_name = client.get('oauth_client', {}).get(
  302. 'android_info', {}).get('package_name', '')
  303. if package_name:
  304. if not selected_package_name:
  305. selected_package_name = package_name
  306. selected_client = client
  307. if package_name == find_package_name:
  308. selected_package_name = package_name
  309. selected_client = client
  310. packages.add(package_name)
  311. if args.p and selected_package_name != find_package_name:
  312. sys.stderr.write('No packages found in %s which match the package '
  313. 'name %s\n'
  314. '\n'
  315. 'Found the following:\n'
  316. '%s\n' % (input_filename, find_package_name,
  317. '\n'.join(packages)))
  318. return 1
  319. client_api_key = selected_client.get('api_key')
  320. if client_api_key:
  321. client_api_key0 = client_api_key[0]
  322. gen_string(root, 'google_api_key', client_api_key0.get('current_key'))
  323. gen_string(root, 'google_crash_reporting_api_key',
  324. client_api_key0.get('current_key'))
  325. client_info = selected_client.get('client_info')
  326. if client_info:
  327. gen_string(root, 'google_app_id', client_info.get('mobilesdk_app_id'))
  328. oauth_client_list = selected_client.get('oauth_client')
  329. if oauth_client_list:
  330. for oauth_client in oauth_client_list:
  331. client_type = oauth_client.get('client_type')
  332. client_id = oauth_client.get('client_id')
  333. if client_type and client_type == OAUTH_CLIENT_TYPE_WEB and client_id:
  334. gen_string(root, 'default_web_client_id', client_id)
  335. # Only include the first matching OAuth web client ID.
  336. break
  337. services = selected_client.get('services')
  338. if services:
  339. ads_service = services.get('ads_service')
  340. if ads_service:
  341. gen_string(root, 'test_banner_ad_unit_id',
  342. ads_service.get('test_banner_ad_unit_id'))
  343. gen_string(root, 'test_interstitial_ad_unit_id',
  344. ads_service.get('test_interstitial_ad_unit_id'))
  345. analytics_service = services.get('analytics_service')
  346. if analytics_service:
  347. analytics_property = analytics_service.get('analytics_property')
  348. if analytics_property:
  349. gen_string(root, 'ga_trackingId',
  350. analytics_property.get('tracking_id'))
  351. # enable this once we have an example if this service being present
  352. # in the json data:
  353. maps_service_enabled = False
  354. if maps_service_enabled:
  355. maps_service = services.get('maps_service')
  356. if maps_service:
  357. maps_api_key = maps_service.get('api_key')
  358. if maps_api_key:
  359. for k in range(0, len(maps_api_key)):
  360. # generates potentially multiple of these keys, which is
  361. # the same behavior as the java plugin.
  362. gen_string(root, 'google_maps_key',
  363. maps_api_key[k].get('maps_api_key'))
  364. tree = ElementTree.ElementTree(root)
  365. indent(root)
  366. if args.l:
  367. for package in packages:
  368. if package:
  369. # Encode the output string in case the system's default encoding differs
  370. # from the encoding of the string being printed.
  371. sys.stdout.write((package + '\n').encode(sys.getdefaultencoding()))
  372. else:
  373. path = os.path.dirname(output_filename)
  374. if path and not os.path.exists(path):
  375. os.makedirs(path)
  376. if not args.plist:
  377. tree.write(output_filename, 'utf-8', True)
  378. else:
  379. with open(output_filename, 'w') as ofile:
  380. ofile.write(json_string)
  381. return 0
  382. if __name__ == '__main__':
  383. sys.exit(main())