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