1#!/usr/bin/env python 2# Copyright 2017 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import argparse 7import fnmatch 8import logging 9import os 10import sys 11 12import telemetry_mini 13 14 15BROWSER_FLAGS = [ 16 '--enable-remote-debugging', 17 '--disable-fre', 18 '--no-default-browser-check', 19 '--no-first-run', 20] 21 22TRACE_CONFIG = { 23 'excludedCategories': ['*'], 24 'includedCategories': ['rails', 'toplevel', 'startup', 'blink.user_timing'], 25 'memoryDumpConfig': {'triggers': []} 26} 27 28BROWSERS = { 29 'android-chrome': telemetry_mini.ChromeApp, 30 'android-chromium': telemetry_mini.ChromiumApp, 31 'android-system-chrome': telemetry_mini.SystemChromeApp, 32} 33 34 35class TwitterApp(telemetry_mini.AndroidApp): 36 PACKAGE_NAME = 'com.twitter.android' 37 38 39class InstagramApp(telemetry_mini.AndroidApp): 40 PACKAGE_NAME = 'com.instagram.android' 41 42 43class HangoutsApp(telemetry_mini.AndroidApp): 44 PACKAGE_NAME = 'com.google.android.talk' 45 46 47class TwitterFlipkartStory(telemetry_mini.UserStory): 48 """Load Chrome Custom Tab from another application. 49 50 The flow of the story is: 51 - Start Twitter app to view the @flipkart profile. 52 - Tap on a link to open Flipkart in a Chrome Custom Tab. 53 - Return to Twitter app. 54 """ 55 NAME = 'twitter_flipkart' 56 FLIPKART_TWITTER_LINK = [ 57 ('package', 'com.twitter.android'), 58 ('class', 'android.widget.TextView'), 59 ('text', 'flipkart.com') 60 ] 61 62 def __init__(self, *args, **kwargs): 63 super(TwitterFlipkartStory, self).__init__(*args, **kwargs) 64 self.watcher = ProcessWatcher(self.device) 65 self.twitter = TwitterApp(self.device) 66 67 def RunPrepareSteps(self): 68 self.twitter.ForceStop() 69 70 def RunStorySteps(self): 71 # Activity will launch Twitter app on Flipkart profile. 72 self.actions.StartActivity('https://twitter.com/flipkart') 73 self.watcher.StartWatching(self.twitter) 74 75 # Tapping on Flikpart link on Twitter app will launch Chrome. 76 self.actions.TapUiElement(self.FLIPKART_TWITTER_LINK) 77 self.watcher.StartWatching(self.browser) 78 self.browser.WaitForCurrentPageReady() 79 self.actions.SwipeUp(repeat=3) 80 81 # Return to Twitter app. 82 self.actions.GoBack() 83 self.watcher.AssertAllAlive() 84 85 def RunCleanupSteps(self): 86 self.twitter.ForceStop() 87 88 89class FlipkartInstagramStory(telemetry_mini.UserStory): 90 """Interaction between Chrome, PWAs and a WebView-based app. 91 92 The flow of the story is: 93 - Launch the Flipkart PWA. 94 - Go back home and launch the Instagram app. 95 - Use the app switcher to return to Flipkart. 96 - Go back home and launch Cricbuzz from a shortcut. 97 """ 98 NAME = 'flipkart_instagram' 99 100 def __init__(self, *args, **kwargs): 101 super(FlipkartInstagramStory, self).__init__(*args, **kwargs) 102 self.watcher = ProcessWatcher(self.device) 103 self.instagram = InstagramApp(self.device) 104 105 def RunPrepareSteps(self): 106 self.instagram.ForceStop() 107 self.actions.ClearRecentApps() 108 109 def RunStorySteps(self): 110 # Tap on home screen shortcut to open Flipkart PWA. 111 self.actions.TapHomeScreenShortcut('Flipkart Lite') 112 self.watcher.StartWatching(self.browser) 113 self.browser.WaitForCurrentPageReady() 114 self.actions.SwipeUp(repeat=2) 115 116 # Go back home, then launch Instagram app. 117 self.actions.GoHome() 118 self.actions.TapHomeScreenShortcut('Instagram') 119 self.watcher.StartWatching(self.instagram) 120 self.actions.SwipeUp(repeat=5) 121 122 # Go to app switcher and return to Flipkart PWA. 123 self.actions.GoAppSwitcher() 124 self.actions.TapAppSwitcherTitle('Flipkart Lite') 125 self.actions.SwipeDown() 126 127 # Go back home, then open Cricbuzz shortcut. 128 self.actions.GoHome() 129 self.actions.TapHomeScreenShortcut('Cricbuzz') 130 self.browser.WaitForCurrentPageReady() 131 self.actions.SwipeUp() 132 self.watcher.AssertAllAlive() 133 134 def RunCleanupSteps(self): 135 self.instagram.ForceStop() 136 137 138class HangoutsIndiaTimesStory(telemetry_mini.UserStory): 139 """Interaction between Chrome and a non-WebView-based app. 140 141 TODO: Not sure if Hangouts is a non-WebView app. Consider using another app 142 if needed. 143 144 The flow of the story is: 145 - Launch the Hangouts app. 146 - Open a conversation with a link to an IndiaTimes article. 147 - Click on the link to launch Chrome. 148 - Go back to the conversation. 149 """ 150 NAME = 'hangouts_indiatimes' 151 # TODO: Maybe use more specific targets, e.g. check url is in message. 152 FIRST_CONVERSATION = [ 153 ('resource-id', 'com.google.android.talk:id/conversationContent'), 154 ('index', '0'), 155 ] 156 SECOND_MESSAGE = [ 157 ('resource-id', 'com.google.android.talk:id/message_root'), 158 ('index', '1'), 159 ] 160 161 def __init__(self, *args, **kwargs): 162 super(HangoutsIndiaTimesStory, self).__init__(*args, **kwargs) 163 self.watcher = ProcessWatcher(self.device) 164 self.hangouts = HangoutsApp(self.device) 165 166 def RunPrepareSteps(self): 167 self.hangouts.ForceStop() 168 169 def RunStorySteps(self): 170 # Tap on home screen shortcut to open Hangouts app. 171 self.actions.TapHomeScreenShortcut('Hangouts') 172 self.watcher.StartWatching(self.hangouts) 173 174 # Find conversation with link to IndiaTimes and tap to launch Chrome. 175 self.actions.TapUiElement(self.FIRST_CONVERSATION) 176 self.actions.TapUiElement(self.SECOND_MESSAGE) 177 self.watcher.StartWatching(self.browser) 178 self.browser.WaitForCurrentPageReady() 179 self.actions.SwipeUp(repeat=4) 180 181 # Go back to Hangouts, then back Home. 182 self.actions.GoBack() 183 self.actions.Idle(2) 184 self.actions.GoHome() 185 self.watcher.AssertAllAlive() 186 187 def RunCleanupSteps(self): 188 self.hangouts.ForceStop() 189 190 191STORIES = ( 192 TwitterFlipkartStory, 193 FlipkartInstagramStory, 194 HangoutsIndiaTimesStory, 195) 196 197 198class ProcessWatcher(object): 199 def __init__(self, device): 200 self.device = device 201 self._process_pid = {} 202 203 def StartWatching(self, process_name): 204 """Register a process or android app to keep track of its PID.""" 205 if isinstance(process_name, telemetry_mini.AndroidApp): 206 process_name = process_name.PACKAGE_NAME 207 208 @telemetry_mini.RetryOn(returns_falsy=True) 209 def GetPids(): 210 # Returns an empty list if the process name is not found. 211 return self.device.ProcessStatus()[process_name] 212 213 assert process_name not in self._process_pid 214 pids = GetPids() 215 assert pids, 'PID for %s not found' % process_name 216 assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( 217 process_name, pids) 218 logging.info('Started watching %s (PID=%d)', process_name, pids[0]) 219 self._process_pid[process_name] = pids[0] 220 221 def AssertAllAlive(self): 222 """Check that all watched processes remain alive and were not restarted.""" 223 status = self.device.ProcessStatus() 224 all_alive = True 225 for process_name, old_pid in sorted(self._process_pid.iteritems()): 226 new_pids = status[process_name] 227 if not new_pids: 228 all_alive = False 229 logging.error('Process %s died (PID=%d).', process_name, old_pid) 230 elif new_pids != [old_pid]: 231 all_alive = False 232 logging.error( 233 'Process %s restarted (PID=%d -> %s).', process_name, 234 old_pid, new_pids) 235 else: 236 logging.info('Process %s still alive (PID=%d)', process_name, old_pid) 237 assert all_alive, 'Some watched processes died or got restarted' 238 239 240def EnsureSingleBrowser(device, browser_name, force_install=False): 241 """Ensure a single Chrome browser is installed and available on the device. 242 243 Having more than one Chrome browser available may produce results which are 244 confusing or unreliable (e.g. unclear which browser will respond by default 245 to intents triggered by other apps). 246 247 This function ensures only the selected browser is available, installing it 248 if necessary, and uninstalling/disabling others. 249 """ 250 browser = BROWSERS[browser_name](device) 251 available_browsers = set(device.ListPackages('chrome', only_enabled=True)) 252 253 # Install or enable if needed. 254 if force_install or browser.PACKAGE_NAME not in available_browsers: 255 browser.Install() 256 257 # Uninstall disable other browser apps. 258 for other_browser in BROWSERS.itervalues(): 259 if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and 260 other_browser.PACKAGE_NAME in available_browsers): 261 other_browser(device).Uninstall() 262 263 # Finally check that only the selected browser is actually available. 264 available_browsers = device.ListPackages('chrome', only_enabled=True) 265 assert browser.PACKAGE_NAME in available_browsers, ( 266 'Unable to make %s available' % browser.PACKAGE_NAME) 267 available_browsers.remove(browser.PACKAGE_NAME) 268 assert not available_browsers, ( 269 'Other browsers may intefere with the test: %s' % available_browsers) 270 return browser 271 272 273def main(): 274 browser_names = sorted(BROWSERS) 275 default_browser = 'android-chrome' 276 parser = argparse.ArgumentParser() 277 parser.add_argument('--serial', 278 help='device serial on which to run user stories' 279 ' (defaults to first device found)') 280 parser.add_argument('--adb-bin', default='adb', metavar='PATH', 281 help='path to adb binary to use (default: %(default)s)') 282 parser.add_argument('--browser', default=default_browser, metavar='NAME', 283 choices=browser_names, 284 help='one of: %s' % ', '.join( 285 '%s (default)' % b if b == default_browser else b 286 for b in browser_names)) 287 parser.add_argument('--story-filter', metavar='PATTERN', default='*', 288 help='run the matching stories only (allows Unix' 289 ' shell-style wildcards)') 290 parser.add_argument('--repeat', metavar='NUM', type=int, default=1, 291 help='repeat the story set a number of times' 292 ' (default: %(default)d)') 293 parser.add_argument('--output-dir', metavar='PATH', 294 help='path to directory for placing output trace files' 295 ' (defaults to current directory)') 296 parser.add_argument('--force-install', action='store_true', 297 help='install APK even if browser is already available') 298 parser.add_argument('--apks-dir', metavar='PATH', 299 help='path where to find APKs to install') 300 parser.add_argument('--port', type=int, default=1234, 301 help='port for connection with device' 302 ' (default: %(default)s)') 303 parser.add_argument('-v', '--verbose', action='store_true') 304 args = parser.parse_args() 305 306 logging.basicConfig() 307 if args.verbose: 308 logging.getLogger().setLevel(logging.INFO) 309 310 stories = [s for s in STORIES if fnmatch.fnmatch(s.NAME, args.story_filter)] 311 if not stories: 312 return 'No matching stories' 313 314 if args.output_dir is None: 315 args.output_dir = os.getcwd() 316 else: 317 args.output_dir = os.path.realpath(args.output_dir) 318 if not os.path.isdir(args.output_dir): 319 return 'Output directory does not exit' 320 321 if args.apks_dir is None: 322 args.apks_dir = os.path.realpath(os.path.join( 323 os.path.dirname(__file__), '..', '..', '..', '..', 324 'out', 'Release', 'apks')) 325 telemetry_mini.AndroidApp.APKS_DIR = args.apks_dir 326 327 telemetry_mini.AdbMini.ADB_BIN = args.adb_bin 328 if args.serial is None: 329 device = next(telemetry_mini.AdbMini.GetDevices()) 330 logging.warning( 331 'Connected to first device found: --serial %s', device.serial) 332 else: 333 device = telemetry_mini.AdbMini(args.serial) 334 335 # Some operations may require a rooted device. 336 device.RunCommand('root') 337 device.RunCommand('wait-for-device') 338 339 browser = EnsureSingleBrowser(device, args.browser, args.force_install) 340 browser.SetBrowserFlags(BROWSER_FLAGS) 341 browser.SetTraceConfig(TRACE_CONFIG) 342 browser.SetDevToolsLocalPort(args.port) 343 telemetry_mini.RunStories(browser, stories, args.repeat, args.output_dir) 344 345 346if __name__ == '__main__': 347 sys.exit(main()) 348