#!/usr/bin/python3 ## # A good background read on how Android handles alternative resources is here: # https://developer.android.com/guide/topics/resources/providing-resources.html # # This uses lxml so you may need to install it manually if your distribution # does not ordinarily ship with it. On Ubuntu, you can run: # # sudo apt-get install python-lxml # # Example invocation: # ./resource_generator.py --csv specs/keylines.csv --resdir car-stream-ui-lib/res --type dimens ## import argparse import csv import datetime import os import pprint import lxml.etree as et DBG = False class ResourceGenerator: def __init__(self): self.COLORS = "colors" self.DIMENS = "dimens" self.TAG_DIMEN = "dimen" self.resource_handlers = { self.COLORS : self.HandleColors, self.DIMENS : self.HandleDimens, } self.ENCODING = "utf-8" self.XML_HEADER = '' % self.ENCODING # The indentation looks off but it needs to be otherwise the indentation will end up in the # string itself, which we don't want. So much for pythons indentation policy. self.AOSP_HEADER = """ """ % datetime.datetime.now().year self.EMPTY_XML = "" def HandleColors(self, reader, resource_dir): raise Exception("Not yet implemented") ## # Validate the header row of the csv. Getting this wrong would mean that the resources wouldn't # actually work, so find any mistakes ahead of time. ## def ValidateHeader(self, header): # TODO: Validate the header values based on the ordering of modifiers in table 2. pass ## # Given a list of resource modifers, create the appropriate directories and xml files for # them to be populated in. # Returns a tuple of maps of the form ({ modifier : xml file } , { modifier : xml object }) ## def CreateOrOpenResourceFiles(self, resource_dir, resource_type, modifiers): filenames = { } xmltrees = { } dir_prefix = "values" qualifier_separator = "-" file_extension = ".xml" for modifier in modifiers: # We're using the keyword none to specify that there are no modifiers and so the # values specified here goes into the default file. directory = resource_dir + os.path.sep + dir_prefix if modifier != "none": directory = directory + qualifier_separator + modifier if not os.path.exists(directory): if DBG: print("Creating directory %s" % directory) os.mkdir(directory) filename = directory + os.path.sep + resource_type + file_extension if not os.path.exists(filename): if DBG: print("Creating file %s" % filename) with open(filename, "w") as xmlfile: xmlfile.write(self.XML_HEADER) xmlfile.write(self.AOSP_HEADER) xmlfile.write(self.EMPTY_XML) filenames[modifier] = filename if DBG: print("Parsing %s" % (filename)) parser = et.XMLParser(remove_blank_text=True) xmltrees[modifier] = et.parse(filename, parser) return filenames, xmltrees ## # Updates a resource value in the xmltree if it exists, adds it in if not. ## def AddOrUpdateValue(self, xmltree, tag, resource_name, resource_value): root = xmltree.getroot() found = False resource_node = None attr_name = "name" # Look for the value that we want. for elem in root: if elem.tag == tag and elem.attrib[attr_name] == resource_name: resource_node = elem found = True break # If it doesn't exist yet, create one. if not found: resource_node = et.SubElement(root, tag) resource_node.attrib[attr_name] = resource_name # Update the value. resource_node.text = resource_value ## # lxml formats xml with 2 space indentation. Android convention says 4 spaces. Multiply any # leading spaces by 2 and re-generate the string. ## def FixupIndentation(self, xml_string): reformatted_xml = "" for line in xml_string.splitlines(): stripped = line.lstrip() # Special case for multiline comments. These usually are hand aligned with something # so we don't want to reformat those. if not stripped.startswith("<"): leading_spaces = 0 else: leading_spaces = len(line) - len(stripped) reformatted_xml += " " * leading_spaces + line + os.linesep return reformatted_xml ## # Read all the lines that appear before the tag so that they can be replicated # while writing out the file again. We can't simply re-generate the aosp header because it's # apparently not a good thing to change the date on a copyright notice to something more # recent. # Returns a string of the lines that appear before the resources tag. ## def ReadStartingLines(self, filename): with open(filename) as f: starting_lines = "" for line in f.readlines(): # Yes, this will fail if you start a line inside a comment with . # It's more work than it's worth to handle that case. if line.lstrip().startswith(" 0: var_values[header[idx]] = cell if len(var_values.keys()) > 0: resources[var_name] = var_values self.ModifyXml(resources, self.DIMENS, resource_dir, self.TAG_DIMEN) ## # Validate the command line arguments that we have been passed. Will raise an exception if # there are any invalid arguments. ## def ValidateArgs(self, csv, resource_dir, resource_type): if not os.path.isfile(csv): raise ValueError("%s is not a valid path" % csv) if not os.path.isdir(resource_dir): raise ValueError("%s is not a valid resource directory" % resource_dir) if not resource_type in self.resource_handlers: raise ValueError("%s is not a supported resource type" % resource_type) ## # The logical entry point of this application. ## def Main(self, csv_file, resource_dir, resource_type): self.ValidateArgs(csv_file, resource_dir, resource_type) # Will raise if it fails. with open(csv_file, 'r') as handle: reader = csv.reader(handle) # Defaults to the excel dialect of csv. self.resource_handlers[resource_type](reader, resource_dir) print("Done!") if __name__ == "__main__": parser = argparse.ArgumentParser(description='Convert a CSV into android resources') parser.add_argument('--csv', action='store', dest='csv') parser.add_argument('--resdir', action='store', dest='resdir') parser.add_argument('--type', action='store', dest='type') args = parser.parse_args() app = ResourceGenerator() app.Main(args.csv, args.resdir, args.type)