| Index: experimental/telemetry_mini/android_go_stories.py | 
| diff --git a/experimental/telemetry_mini/android_go_stories.py b/experimental/telemetry_mini/android_go_stories.py | 
| index 7d9c57b929c93a3c30713396c359973e7a029078..07a17ed8ebd971752e7af457226feb8f0dc1da95 100755 | 
| --- a/experimental/telemetry_mini/android_go_stories.py | 
| +++ b/experimental/telemetry_mini/android_go_stories.py | 
| @@ -4,6 +4,7 @@ | 
| # found in the LICENSE file. | 
|  | 
| import argparse | 
| +import fnmatch | 
| import logging | 
| import os | 
| import sys | 
| @@ -31,81 +32,6 @@ BROWSERS = { | 
| } | 
|  | 
|  | 
| -class ProcessWatcher(object): | 
| -  def __init__(self, device): | 
| -    self.device = device | 
| -    self._process_pid = {} | 
| - | 
| -  def StartWatching(self, process_name): | 
| -    """Register a process or android app to keep track of its PID.""" | 
| -    if isinstance(process_name, telemetry_mini.AndroidApp): | 
| -      process_name = process_name.PACKAGE_NAME | 
| - | 
| -    @telemetry_mini.RetryOn(returns_falsy=True) | 
| -    def GetPids(): | 
| -      # Returns an empty list if the process name is not found. | 
| -      return self.device.ProcessStatus()[process_name] | 
| - | 
| -    assert process_name not in self._process_pid | 
| -    pids = GetPids() | 
| -    assert pids, 'PID for %s not found' % process_name | 
| -    assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( | 
| -        process_name, pids) | 
| -    logging.info('Started watching %s (PID=%d)', process_name, pids[0]) | 
| -    self._process_pid[process_name] = pids[0] | 
| - | 
| -  def AssertAllAlive(self): | 
| -    """Check that all watched processes remain alive and were not restarted.""" | 
| -    status = self.device.ProcessStatus() | 
| -    all_alive = True | 
| -    for process_name, old_pid in sorted(self._process_pid.iteritems()): | 
| -      new_pids = status[process_name] | 
| -      if not new_pids: | 
| -        all_alive = False | 
| -        logging.error('Process %s died (PID=%d).', process_name, old_pid) | 
| -      elif new_pids != [old_pid]: | 
| -        all_alive = False | 
| -        logging.error( | 
| -            'Process %s restarted (PID=%d -> %s).', process_name, | 
| -            old_pid, new_pids) | 
| -      else: | 
| -        logging.info('Process %s still alive (PID=%d)', process_name, old_pid) | 
| -    assert all_alive, 'Some watched processes died or got restarted' | 
| - | 
| - | 
| -def EnsureSingleBrowser(device, browser_name, force_install=False): | 
| -  """Ensure a single Chrome browser is installed and available on the device. | 
| - | 
| -  Having more than one Chrome browser available may produce results which are | 
| -  confusing or unreliable (e.g. unclear which browser will respond by default | 
| -  to intents triggered by other apps). | 
| - | 
| -  This function ensures only the selected browser is available, installing it | 
| -  if necessary, and uninstalling/disabling others. | 
| -  """ | 
| -  browser = BROWSERS[browser_name](device) | 
| -  available_browsers = set(device.ListPackages('chrome', only_enabled=True)) | 
| - | 
| -  # Install or enable if needed. | 
| -  if force_install or browser.PACKAGE_NAME not in available_browsers: | 
| -    browser.Install() | 
| - | 
| -  # Uninstall disable other browser apps. | 
| -  for other_browser in BROWSERS.itervalues(): | 
| -    if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and | 
| -        other_browser.PACKAGE_NAME in available_browsers): | 
| -      other_browser(device).Uninstall() | 
| - | 
| -  # Finally check that only the selected browser is actually available. | 
| -  available_browsers = device.ListPackages('chrome', only_enabled=True) | 
| -  assert browser.PACKAGE_NAME in available_browsers, ( | 
| -      'Unable to make %s available' % browser.PACKAGE_NAME) | 
| -  available_browsers.remove(browser.PACKAGE_NAME) | 
| -  assert not available_browsers, ( | 
| -      'Other browsers may intefere with the test: %s' % available_browsers) | 
| -  return browser | 
| - | 
| - | 
| class TwitterApp(telemetry_mini.AndroidApp): | 
| PACKAGE_NAME = 'com.twitter.android' | 
|  | 
| @@ -115,6 +41,14 @@ class InstagramApp(telemetry_mini.AndroidApp): | 
|  | 
|  | 
| class TwitterFlipkartStory(telemetry_mini.UserStory): | 
| +  """Load Chrome Custom Tab from another application. | 
| + | 
| +  The flow of the story is: | 
| +  - Start Twitter app to view the @flipkart profile. | 
| +  - Tap on a link to open Flipkart in a Chrome Custom Tab. | 
| +  - Return to Twitter app. | 
| +  """ | 
| +  NAME = 'twitter_flipkart' | 
| FLIPKART_TWITTER_LINK = [ | 
| ('package', 'com.twitter.android'), | 
| ('class', 'android.widget.TextView'), | 
| @@ -149,6 +83,16 @@ class TwitterFlipkartStory(telemetry_mini.UserStory): | 
|  | 
|  | 
| class FlipkartInstagramStory(telemetry_mini.UserStory): | 
| +  """Interaction between Chrome, PWAs and a WebView-based app. | 
| + | 
| +  The flow of the story is: | 
| +  - Launch the Flipkart PWA. | 
| +  - Go back home and launch the Instagram app. | 
| +  - Use the app switcher to return to Flipkart. | 
| +  - Go back home and launch Cricbuzz from a shortcut. | 
| +  """ | 
| +  NAME = 'flipkart_instagram' | 
| + | 
| def __init__(self, *args, **kwargs): | 
| super(FlipkartInstagramStory, self).__init__(*args, **kwargs) | 
| self.watcher = ProcessWatcher(self.device) | 
| @@ -187,6 +131,87 @@ class FlipkartInstagramStory(telemetry_mini.UserStory): | 
| self.instagram.ForceStop() | 
|  | 
|  | 
| +STORIES = ( | 
| +    TwitterFlipkartStory, | 
| +    FlipkartInstagramStory | 
| +) | 
| + | 
| + | 
| +class ProcessWatcher(object): | 
| +  def __init__(self, device): | 
| +    self.device = device | 
| +    self._process_pid = {} | 
| + | 
| +  def StartWatching(self, process_name): | 
| +    """Register a process or android app to keep track of its PID.""" | 
| +    if isinstance(process_name, telemetry_mini.AndroidApp): | 
| +      process_name = process_name.PACKAGE_NAME | 
| + | 
| +    @telemetry_mini.RetryOn(returns_falsy=True) | 
| +    def GetPids(): | 
| +      # Returns an empty list if the process name is not found. | 
| +      return self.device.ProcessStatus()[process_name] | 
| + | 
| +    assert process_name not in self._process_pid | 
| +    pids = GetPids() | 
| +    assert pids, 'PID for %s not found' % process_name | 
| +    assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( | 
| +        process_name, pids) | 
| +    logging.info('Started watching %s (PID=%d)', process_name, pids[0]) | 
| +    self._process_pid[process_name] = pids[0] | 
| + | 
| +  def AssertAllAlive(self): | 
| +    """Check that all watched processes remain alive and were not restarted.""" | 
| +    status = self.device.ProcessStatus() | 
| +    all_alive = True | 
| +    for process_name, old_pid in sorted(self._process_pid.iteritems()): | 
| +      new_pids = status[process_name] | 
| +      if not new_pids: | 
| +        all_alive = False | 
| +        logging.error('Process %s died (PID=%d).', process_name, old_pid) | 
| +      elif new_pids != [old_pid]: | 
| +        all_alive = False | 
| +        logging.error( | 
| +            'Process %s restarted (PID=%d -> %s).', process_name, | 
| +            old_pid, new_pids) | 
| +      else: | 
| +        logging.info('Process %s still alive (PID=%d)', process_name, old_pid) | 
| +    assert all_alive, 'Some watched processes died or got restarted' | 
| + | 
| + | 
| +def EnsureSingleBrowser(device, browser_name, force_install=False): | 
| +  """Ensure a single Chrome browser is installed and available on the device. | 
| + | 
| +  Having more than one Chrome browser available may produce results which are | 
| +  confusing or unreliable (e.g. unclear which browser will respond by default | 
| +  to intents triggered by other apps). | 
| + | 
| +  This function ensures only the selected browser is available, installing it | 
| +  if necessary, and uninstalling/disabling others. | 
| +  """ | 
| +  browser = BROWSERS[browser_name](device) | 
| +  available_browsers = set(device.ListPackages('chrome', only_enabled=True)) | 
| + | 
| +  # Install or enable if needed. | 
| +  if force_install or browser.PACKAGE_NAME not in available_browsers: | 
| +    browser.Install() | 
| + | 
| +  # Uninstall disable other browser apps. | 
| +  for other_browser in BROWSERS.itervalues(): | 
| +    if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and | 
| +        other_browser.PACKAGE_NAME in available_browsers): | 
| +      other_browser(device).Uninstall() | 
| + | 
| +  # Finally check that only the selected browser is actually available. | 
| +  available_browsers = device.ListPackages('chrome', only_enabled=True) | 
| +  assert browser.PACKAGE_NAME in available_browsers, ( | 
| +      'Unable to make %s available' % browser.PACKAGE_NAME) | 
| +  available_browsers.remove(browser.PACKAGE_NAME) | 
| +  assert not available_browsers, ( | 
| +      'Other browsers may intefere with the test: %s' % available_browsers) | 
| +  return browser | 
| + | 
| + | 
| def main(): | 
| browser_names = sorted(BROWSERS) | 
| default_browser = 'android-chrome' | 
| @@ -201,6 +226,15 @@ def main(): | 
| help='one of: %s' % ', '.join( | 
| '%s (default)' % b if b == default_browser else b | 
| for b in browser_names)) | 
| +  parser.add_argument('--story-filter', metavar='PATTERN', default='*', | 
| +                      help='run the matching stories only (allows Unix' | 
| +                      ' shell-style wildcards)') | 
| +  parser.add_argument('--repeat', metavar='NUM', type=int, default=1, | 
| +                      help='repeat the story set a number of times' | 
| +                      ' (default: %(default)d)') | 
| +  parser.add_argument('--output-dir', metavar='PATH', | 
| +                      help='path to directory for placing output trace files' | 
| +                      ' (defaults to current directory)') | 
| parser.add_argument('--force-install', action='store_true', | 
| help='install APK even if browser is already available') | 
| parser.add_argument('--apks-dir', metavar='PATH', | 
| @@ -215,6 +249,17 @@ def main(): | 
| if args.verbose: | 
| logging.getLogger().setLevel(logging.INFO) | 
|  | 
| +  stories = [s for s in STORIES if fnmatch.fnmatch(s.NAME, args.story_filter)] | 
| +  if not stories: | 
| +    return 'No matching stories' | 
| + | 
| +  if args.output_dir is None: | 
| +    args.output_dir = os.getcwd() | 
| +  else: | 
| +    args.output_dir = os.path.realpath(args.output_dir) | 
| +  if not os.path.isdir(args.output_dir): | 
| +    return 'Output directory does not exit' | 
| + | 
| if args.apks_dir is None: | 
| args.apks_dir = os.path.realpath(os.path.join( | 
| os.path.dirname(__file__), '..', '..', '..', '..', | 
| @@ -233,10 +278,10 @@ def main(): | 
| device.RunCommand('wait-for-device') | 
|  | 
| browser = EnsureSingleBrowser(device, args.browser, args.force_install) | 
| +  browser.SetBrowserFlags(BROWSER_FLAGS) | 
| +  browser.SetTraceConfig(TRACE_CONFIG) | 
| browser.SetDevToolsLocalPort(args.port) | 
| - | 
| -  story = FlipkartInstagramStory(browser) | 
| -  story.Run(BROWSER_FLAGS, TRACE_CONFIG, 'trace.json') | 
| +  telemetry_mini.RunStories(browser, stories, args.repeat, args.output_dir) | 
|  | 
|  | 
| if __name__ == '__main__': | 
|  |