diff --git a/.hgignore b/.hgignore index 3bf9706a8..1c3f1f5a7 100644 --- a/.hgignore +++ b/.hgignore @@ -5,3 +5,5 @@ syntax:glob */proguard/** .metadata MyTracksTest/annotations.log +**/*.pyc +**/*.pyo diff --git a/MyTracks/res/values-pt-rBR/strings.xml b/MyTracks/res/values-pt-rBR/strings.xml index 2f60a4a9c..46f347e11 100644 --- a/MyTracks/res/values-pt-rBR/strings.xml +++ b/MyTracks/res/values-pt-rBR/strings.xml @@ -96,6 +96,7 @@ limitations under the License. Não Sim + Erro Successo Ponto de passagem diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index cfa4c85ef..254f47df0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -362,6 +362,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.d(TAG, "MyTracks.onStart"); super.onStart(); dataHub.start(); + + // Ensure that service is running if we're supposed to be recording + if (dataHub.isRecording()) { + Intent startIntent = new Intent(this, TrackRecordingService.class); + startService(startIntent); + } } /* diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java index d165f0e1e..8833d096e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java @@ -25,7 +25,6 @@ import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.services.TrackRecordingService; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.mytracks.R; @@ -281,17 +280,7 @@ public class TrackDataHub { started = true; sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceListener); - selectedTrackId = sharedPreferences.getLong(SELECTED_TRACK_KEY, -1); - recordingTrackId = sharedPreferences.getLong(RECORDING_TRACK_KEY, -1); - useMetricUnits = sharedPreferences.getBoolean(METRIC_UNITS_KEY, true); - reportSpeed = sharedPreferences.getBoolean(SPEED_REPORTING_KEY, true); - minRequiredAccuracy = sharedPreferences.getInt(MIN_REQUIRED_ACCURACY_KEY, - Constants.DEFAULT_MIN_REQUIRED_ACCURACY); - - if (recordingTrackId > 0) { - Intent startIntent = new Intent(context, TrackRecordingService.class); - context.startService(startIntent); - } + loadSharedPreferences(); // This may or may not register internal listeners, depending on whether // we already had external listeners. @@ -302,6 +291,15 @@ public class TrackDataHub { reloadDataFor(getRegisteredListenerArray()); } + private void loadSharedPreferences() { + selectedTrackId = sharedPreferences.getLong(SELECTED_TRACK_KEY, -1); + recordingTrackId = sharedPreferences.getLong(RECORDING_TRACK_KEY, -1); + useMetricUnits = sharedPreferences.getBoolean(METRIC_UNITS_KEY, true); + reportSpeed = sharedPreferences.getBoolean(SPEED_REPORTING_KEY, true); + minRequiredAccuracy = sharedPreferences.getInt(MIN_REQUIRED_ACCURACY_KEY, + Constants.DEFAULT_MIN_REQUIRED_ACCURACY); + } + /** * Stops listening to data sources and reporting the data to external * listeners. @@ -396,26 +394,31 @@ public class TrackDataHub { /** Returns the ID of the currently-selected track. */ public long getSelectedTrackId() { - checkStarted(); + if (!started) { + loadSharedPreferences(); + } return selectedTrackId; } /** Returns whether there's a track currently selected. */ public boolean isATrackSelected() { - checkStarted(); - return selectedTrackId > 0; + return getSelectedTrackId() > 0; } /** Returns whether we're currently recording a track. */ public boolean isRecording() { - checkStarted(); + if (!started) { + loadSharedPreferences(); + } return recordingTrackId > 0; } /** Returns whether the selected track is still being recorded. */ public boolean isRecordingSelected() { - checkStarted(); - return isRecording() && recordingTrackId == selectedTrackId; + if (!started) { + loadSharedPreferences(); + } + return recordingTrackId > 0 && recordingTrackId == selectedTrackId; } /** diff --git a/scripts/i18n/.project b/scripts/i18n/.project new file mode 100644 index 000000000..c21d8dd65 --- /dev/null +++ b/scripts/i18n/.project @@ -0,0 +1,17 @@ + + + i18n + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/scripts/i18n/.pydevproject b/scripts/i18n/.pydevproject new file mode 100644 index 000000000..94dbecf08 --- /dev/null +++ b/scripts/i18n/.pydevproject @@ -0,0 +1,10 @@ + + + + +Default +python 2.6 + +/i18n/src + + diff --git a/scripts/i18n/src/mytracks/__init__.py b/scripts/i18n/src/mytracks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/i18n/src/mytracks/files.py b/scripts/i18n/src/mytracks/files.py new file mode 100644 index 000000000..1ec50da8a --- /dev/null +++ b/scripts/i18n/src/mytracks/files.py @@ -0,0 +1,45 @@ +''' +Module for dealing with resource files (but not their contents). + +@author: Rodrigo Damazio +''' +import os.path +from glob import glob +import re + +MYTRACKS_RES_DIR = 'MyTracks/res' +ANDROID_MASTER_VALUES = 'values' +ANDROID_VALUES_MASK = 'values-*' + + +def GetMyTracksDir(): + ''' + Returns the directory in which the MyTracks directory is located. + ''' + path = os.getcwd() + while not os.path.isdir(os.path.join(path, MYTRACKS_RES_DIR)): + if path == '/': + raise 'Not in My Tracks project' + + # Go up one level + path = os.path.split(path)[0] + + return path + + +def GetAllLanguageFiles(): + ''' + Returns a mapping from all found languages to their respective directories. + ''' + mytracks_path = GetMyTracksDir() + res_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_VALUES_MASK) + language_dirs = glob(res_dir) + master_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_MASTER_VALUES) + if len(language_dirs) == 0: + raise 'No languages found!' + if not os.path.isdir(master_dir): + raise 'Couldn\'t find master file' + + language_tuples = [(re.findall(r'.*values-([A-Za-z-]+)', dir)[0],dir) for dir in language_dirs] + language_tuples.append(('en', master_dir)) + return dict(language_tuples) diff --git a/scripts/i18n/src/mytracks/history.py b/scripts/i18n/src/mytracks/history.py new file mode 100644 index 000000000..a585a0105 --- /dev/null +++ b/scripts/i18n/src/mytracks/history.py @@ -0,0 +1,94 @@ +''' +Module which brings history information about files from Mercurial. + +@author: Rodrigo Damazio +''' + +import re +import subprocess + +REVISION_REGEX = re.compile(r'(?P[0-9a-f]{12}):.*') + +def _GetOutputLines(args): + ''' + Runs an external process and returns its output as a list of lines. + + @param args: the arguments to run + ''' + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + universal_newlines = True, + shell = False) + output = process.communicate()[0] + return output.splitlines() + + +def FillMercurialRevisions(filename, parsed_file): + ''' + Fills the revs attribute of all strings in the given parsed file with + a list of revisions that touched the lines corresponding to that string. + + @param filename: the name of the file to get history for + @param parsed_file: the parsed file to modify + ''' + # Take output of hg annotate to get revision of each line + output_lines = _GetOutputLines(['hg', 'annotate', '-c', filename]) + + # Create a map of line -> revision (key is list index, line 0 doesn't exist) + line_revs = ['dummy'] + for line in output_lines: + rev_match = REVISION_REGEX.match(line) + if not rev_match: + raise 'Unexpected line of output from hg: %s' % line + rev_hash = rev_match.group('hash') + line_revs.append(rev_hash) + + for str in parsed_file.itervalues(): + # Get the lines that correspond to each string + start_line = str['startLine'] + end_line = str['endLine'] + + # Get the revisions that touched those lines + revs = [] + for line_number in range(start_line, end_line + 1): + revs.append(line_revs[line_number]) + + # Merge with any revisions that were already there + # (for explict revision specification) + if 'revs' in str: + revs += str['revs'] + + # Assign the revisions to the string + str['revs'] = frozenset(revs) + +def DoesRevisionSuperceed(filename, rev1, rev2): + ''' + Tells whether a revision superceeds another. + This essentially means that the older revision is an ancestor of the newer + one. + This also returns True if the two revisions are the same. + + @param rev1: the revision that may be superceeding the other + @param rev2: the revision that may be superceeded + @return: True if rev1 superceeds rev2 or they're the same + ''' + if rev1 == rev2: + return True + + # TODO: Add filename + args = ['hg', 'log', '-r', 'ancestors(%s)' % rev1, '--template', '{node|short}\n', filename] + output_lines = _GetOutputLines(args) + + return rev2 in output_lines + +def NewestRevision(filename, rev1, rev2): + ''' + Returns which of two revisions is closest to the head of the repository. + If none of them is the ancestor of the other, then we return either one. + + @param rev1: the first revision + @param rev2: the second revision + ''' + if DoesRevisionSuperceed(filename, rev1, rev2): + return rev1 + return rev2 \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/main.py b/scripts/i18n/src/mytracks/main.py new file mode 100755 index 000000000..4841c6858 --- /dev/null +++ b/scripts/i18n/src/mytracks/main.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +''' +Entry point for My Tracks i18n tool. + +@author: Rodrigo Damazio +''' + +import mytracks.files +import mytracks.translate +import mytracks.validate +import sys + +def Usage(): + print 'Usage: %s [ ...]\n' % sys.argv[0] + print 'Commands are:' + print ' cleanup' + print ' translate' + print ' validate' + sys.exit(1) + + +def Translate(languages): + ''' + Asks the user to interactively translate any missing or oudated strings from + the files for the given languages. + + @param languages: the languages to translate + ''' + validator = mytracks.validate.Validator(languages) + validator.Validate() + missing = validator.missing_in_lang() + outdated = validator.outdated_in_lang() + + for lang in languages: + untranslated = missing[lang] + outdated[lang] + + if len(untranslated) == 0: + continue + + translator = mytracks.translate.Translator(lang) + translator.Translate(untranslated) + + +def Validate(languages): + ''' + Computes and displays errors in the string files for the given languages. + + @param languages: the languages to compute for + ''' + validator = mytracks.validate.Validator(languages) + validator.Validate() + + error_count = 0 + if (validator.valid()): + print 'All files OK' + else: + for lang, missing in validator.missing_in_master().iteritems(): + print 'Missing in master, present in %s: %s:' % (lang, str(missing)) + error_count = error_count + len(missing) + for lang, missing in validator.missing_in_lang().iteritems(): + print 'Missing in %s, present in master: %s:' % (lang, str(missing)) + error_count = error_count + len(missing) + for lang, outdated in validator.outdated_in_lang().iteritems(): + print 'Outdated in %s: %s:' % (lang, str(outdated)) + error_count = error_count + len(outdated) + + return error_count + + +if __name__ == '__main__': + argv = sys.argv + argc = len(argv) + if argc < 2: + Usage() + + languages = mytracks.files.GetAllLanguageFiles() + if argc == 3: + langs = set(argv[2:]) + if not langs.issubset(languages): + raise 'Language(s) not found' + + # Filter just to the languages specified + languages = dict((lang, lang_file) + for lang, lang_file in languages.iteritems() + if lang in langs or lang == 'en' ) + + cmd = argv[1] + if cmd == 'translate': + Translate(languages) + elif cmd == 'validate': + error_count = Validate(languages) + else: + Usage() + error_count = 0 + + print '%d errors found.' % error_count diff --git a/scripts/i18n/src/mytracks/parser.py b/scripts/i18n/src/mytracks/parser.py new file mode 100644 index 000000000..9cfb96de8 --- /dev/null +++ b/scripts/i18n/src/mytracks/parser.py @@ -0,0 +1,115 @@ +''' +Module which parses a string XML file. + +@author: Rodrigo Damazio +''' + +from xml.parsers.expat import ParserCreate +import re +#import xml.etree.ElementTree as ET + +class StringsParser(object): + ''' + Parser for string XML files. + + This object is not thread-safe and should be used for parsing a single file at + a time, only. + ''' + + def Parse(self, file): + ''' + Parses the given file and returns a dictionary mapping keys to an object + with attributes for that key, such as the value, start/end line and explicit + revisions. + + In addition to the standard XML format of the strings file, this parser + supports an annotation inside comments, in one of these formats: + + + + + Such an annotation indicates that we're explicitly inheriting form the + master file (and the optional revision says that this decision is compatible + with the master file up to that revision). + + @param file: the name of the file to parse + ''' + self._Reset() + + # Unfortunately expat is the only parser that will give us line numbers + self._xml_parser = ParserCreate() + self._xml_parser.StartElementHandler = self._StartElementHandler + self._xml_parser.EndElementHandler = self._EndElementHandler + self._xml_parser.CharacterDataHandler = self._CharacterDataHandler + self._xml_parser.CommentHandler = self._CommentHandler + + file_obj = open(file) + self._xml_parser.ParseFile(file_obj) + file_obj.close() + + return self._all_strings + + def _Reset(self): + self._currentString = None + self._currentStringName = None + self._currentStringValue = None + self._all_strings = {} + + def _StartElementHandler(self, name, attrs): + if name != 'string': + return + + if 'name' not in attrs: + return + + assert not self._currentString + assert not self._currentStringName + self._currentString = { + 'startLine' : self._xml_parser.CurrentLineNumber, + } + + if 'rev' in attrs: + self._currentString['revs'] = [attrs['rev']] + + self._currentStringName = attrs['name'] + self._currentStringValue = '' + + def _EndElementHandler(self, name): + if name != 'string': + return + + assert self._currentString + assert self._currentStringName + self._currentString['value'] = self._currentStringValue + self._currentString['endLine'] = self._xml_parser.CurrentLineNumber + self._all_strings[self._currentStringName] = self._currentString + + self._currentString = None + self._currentStringName = None + self._currentStringValue = None + + def _CharacterDataHandler(self, data): + if not self._currentString: + return + + self._currentStringValue += data + + _KEEP_PARENT_REGEX = re.compile(r'\s*KEEP_PARENT\s+' + r'name\s*=\s*[\'"]?(?P[a-z0-9_]+)[\'"]?' + r'(?:\s+rev=[\'"]?(?P[0-9a-f]{12})[\'"]?)?\s*', + re.MULTILINE | re.DOTALL) + + def _CommentHandler(self, data): + keep_parent_match = self._KEEP_PARENT_REGEX.match(data) + if not keep_parent_match: + return + + name = keep_parent_match.group('name') + self._all_strings[name] = { + 'keepParent' : True, + 'startLine' : self._xml_parser.CurrentLineNumber, + 'endLine' : self._xml_parser.CurrentLineNumber + } + rev = keep_parent_match.group('rev') + if rev: + self._all_strings[name]['revs'] = [rev] \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/translate.py b/scripts/i18n/src/mytracks/translate.py new file mode 100644 index 000000000..5e6e45a86 --- /dev/null +++ b/scripts/i18n/src/mytracks/translate.py @@ -0,0 +1,21 @@ +''' +Module which prompts the user for translations and saves them. + +TODO: implement + +@author: Rodrigo Damazio +''' + +class Translator(object): + ''' + classdocs + ''' + + def __init__(self, language): + ''' + Constructor + ''' + self._language = language + + def Translate(self, string_names): + print string_names \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/validate.py b/scripts/i18n/src/mytracks/validate.py new file mode 100644 index 000000000..ba99b8f27 --- /dev/null +++ b/scripts/i18n/src/mytracks/validate.py @@ -0,0 +1,115 @@ +''' +Module which compares languague files to the master file and detects +issues. + +@author: Rodrigo Damazio +''' + +import os +from mytracks.parser import StringsParser +import mytracks.history + +class Validator(object): + + def __init__(self, languages): + ''' + Builds a strings file validator. + + Params: + @param languages: a dictionary mapping each language to its corresponding directory + ''' + self._langs = {} + self._master = None + self._language_paths = languages + + parser = StringsParser() + for lang, lang_dir in languages.iteritems(): + filename = os.path.join(lang_dir, 'strings.xml') + parsed_file = parser.Parse(filename) + mytracks.history.FillMercurialRevisions(filename, parsed_file) + + if lang == 'en': + self._master = parsed_file + else: + self._langs[lang] = parsed_file + + self._Reset() + + def Validate(self): + ''' + Computes whether all the data in the files for the given languages is valid. + ''' + self._Reset() + self._ValidateMissingKeys() + self._ValidateOutdatedKeys() + + def valid(self): + return (len(self._missing_in_master) == 0 and + len(self._missing_in_lang) == 0 and + len(self._outdated_in_lang) == 0) + + def missing_in_master(self): + return self._missing_in_master + + def missing_in_lang(self): + return self._missing_in_lang + + def outdated_in_lang(self): + return self._outdated_in_lang + + def _Reset(self): + # These are maps from language to string name list + self._missing_in_master = {} + self._missing_in_lang = {} + self._outdated_in_lang = {} + + def _ValidateMissingKeys(self): + ''' + Computes whether there are missing keys on either side. + ''' + master_keys = frozenset(self._master.iterkeys()) + for lang, file in self._langs.iteritems(): + keys = frozenset(file.iterkeys()) + missing_in_master = keys - master_keys + missing_in_lang = master_keys - keys + + if len(missing_in_master) > 0: + self._missing_in_master[lang] = missing_in_master + if len(missing_in_lang) > 0: + self._missing_in_lang[lang] = missing_in_lang + + def _ValidateOutdatedKeys(self): + ''' + Computers whether any of the language keys are outdated with relation to the + master keys. + ''' + for lang, file in self._langs.iteritems(): + outdated = [] + for key, str in file.iteritems(): + # Get all revisions that touched master and language files for this + # string. + master_str = self._master[key] + master_revs = master_str['revs'] + lang_revs = str['revs'] + if not master_revs or not lang_revs: + print 'WARNING: No revision for %s in %s' % (key, lang) + continue + + master_file = os.path.join(self._language_paths['en'], 'strings.xml') + lang_file = os.path.join(self._language_paths[lang], 'strings.xml') + + # Assume that the repository has a single head (TODO: check that), + # and as such there is always one revision which superceeds all others. + master_rev = reduce( + lambda r1, r2: mytracks.history.NewestRevision(master_file, r1, r2), + master_revs) + lang_rev = reduce( + lambda r1, r2: mytracks.history.NewestRevision(lang_file, r1, r2), + lang_revs) + + # If the master version is newer than the lang version + if mytracks.history.DoesRevisionSuperceed(lang_file, master_rev, lang_rev): + outdated.append(key) + + if len(outdated) > 0: + self._outdated_in_lang[lang] = outdated