1"""Pages associated with pairing process of Fitbit tracker device.""" 2import shlex 3from typing import List, Optional 4from xml.dom import minidom 5 6from blueberry.utils.ui_pages import errors 7from blueberry.utils.ui_pages import ui_core 8from blueberry.utils.ui_pages import ui_node 9from blueberry.utils.ui_pages.fitbit_companion import constants 10 11# Alias for typing convenience. 12_NodeList = List[ui_node.UINode] 13 14 15class PairRetryPage(ui_core.UIPage): 16 """Fitbit Companion App's page for retry of pairing.""" 17 18 PAGE_RE_TEXT = 'TRY AGAIN' 19 20 def retry(self) -> ui_core.UIPage: 21 """Clicks button to retry pairing. 22 23 Returns: 24 The transformed page. 25 26 Raises: 27 errors.UIError: Fail to find the target node. 28 """ 29 return self.click_node_by_text('TRY AGAIN') 30 31 32class Pairing4DigitPage(ui_core.UIPage): 33 """Fitbit Companion App's page to enter 4 digit pins for pairing.""" 34 35 PAGE_RID = f'{constants.PKG_NAME_ID}/digits' 36 NODE_DIGIT_RIDS = (f'{constants.PKG_NAME_ID}/digit0' 37 f'{constants.PKG_NAME_ID}/digit1' 38 f'{constants.PKG_NAME_ID}/digit2' 39 f'{constants.PKG_NAME_ID}/digit3') 40 41 def input_pins(self, pins: str) -> ui_core.UIPage: 42 """Inputs 4 digit pins required in pairing process. 43 44 Args: 45 pins: 4 digit pins (e.g.: "1234") 46 47 Returns: 48 The transformed page. 49 50 Raises: 51 ValueError: Input pins is not valid. 52 """ 53 if len(pins) != 4: 54 raise ValueError(f'4 digits required here! (input={pins})') 55 56 for digit in pins: 57 self.ctx.ad.adb.shell(shlex.split(f'input text "{digit}"')) 58 59 return self.ctx.page 60 61 62class PairingConfirmPage(ui_core.UIPage): 63 """Fitbit Companion App's page to confirm pairing.""" 64 65 NODE_ALLOW_ACCESS_TEXT = 'Allow access to your contacts and call history' 66 NODE_PAIR_TEXT = 'Pair' 67 68 @classmethod 69 def from_xml(cls, ctx: ui_core.Context, ui_xml: minidom.Document, 70 clickable_nodes: _NodeList, enabled_nodes: _NodeList, 71 all_nodes: _NodeList) -> Optional[ui_core.UIPage]: 72 """Instantiates page object from XML object. 73 74 Args: 75 ctx: Page context object. 76 ui_xml: Parsed XML object. 77 clickable_nodes: Clickable node list from page. 78 enabled_nodes: Enabled node list from page. 79 all_nodes: All node from page. 80 81 Returns: 82 UI page object iff the given XML object can be parsed. 83 """ 84 for node in enabled_nodes: 85 if (node.text == cls.NODE_PAIR_TEXT and 86 node.resource_id == 'android:id/button1'): 87 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 88 89 def confirm(self) -> ui_core.UIPage: 90 """Confirms the action of pairing. 91 92 Returns: 93 The transformed page. 94 """ 95 self.click_node_by_text(self.NODE_ALLOW_ACCESS_TEXT) 96 97 return self.click_node_by_text(self.NODE_PAIR_TEXT) 98 99 100class PairingIntroPage(ui_core.UIPage): 101 """Fitbit Companion App's pages for introduction of product usage.""" 102 103 NODE_TITLE_TEXT_SET = frozenset([ 104 'All set!', 105 'Double tap to wake', 106 'Firmly double-tap', 107 'How to go back', 108 'Swipe down', 109 'Swipe left or right', 110 'Swipe to navigate', 111 'Swipe up', 112 'Try it on', 113 'Wear & care tips', 114 ]) 115 NODE_NEXT_BTN_RID = f'{constants.PKG_NAME_ID}/btn_next' 116 117 @classmethod 118 def from_xml(cls, ctx: ui_core.Context, ui_xml: minidom.Document, 119 clickable_nodes: _NodeList, enabled_nodes: _NodeList, 120 all_nodes: _NodeList) -> Optional[ui_core.UIPage]: 121 """Instantiates page object from XML object. 122 123 The appending punctuation '.' of the text will be ignored during comparison. 124 125 Args: 126 ctx: Page context object. 127 ui_xml: Parsed XML object. 128 clickable_nodes: Clickable node list from page. 129 enabled_nodes: Enabled node list from page. 130 all_nodes: All node from page. 131 132 Returns: 133 UI page object iff the given XML object can be parsed. 134 """ 135 for node in enabled_nodes: 136 node_text = node.text[:-1] if node.text.endswith('.') else node.text 137 if node_text in cls.NODE_TITLE_TEXT_SET: 138 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 139 140 def next(self): 141 """Moves to next page.""" 142 return self.click_node_by_rid(self.NODE_NEXT_BTN_RID) 143 144 145class PairAndLinkPage(ui_core.UIPage): 146 """Fitbit Companion App's landing page for pairing and linking.""" 147 148 PAGE_TEXT = 'Bluetooth Pairing and Linking' 149 NODE_CANCEL_TEXT = 'Cancel' 150 151 def cancel(self) -> ui_core.UIPage: 152 """Cancel pairing process. 153 154 Returns: 155 The transformed page. 156 """ 157 return self.click_node_by_text(self.NODE_CANCEL_TEXT) 158 159 160class PremiumPage(ui_core.UIPage): 161 """Fitbit Companion App's page for Premium information.""" 162 163 PAGE_TEXT = 'See all Premium features' 164 NODE_EXIT_IMG_BTN_CLASS = 'android.widget.ImageButton' 165 166 def done(self): 167 """Completes pairing process. 168 169 Returns: 170 The transformed page. 171 """ 172 return self.click_node_by_class(self.NODE_EXIT_IMG_BTN_CLASS) 173 174 175class PairPrivacyConfirmPage(ui_core.UIPage): 176 """Fitbit Companion App's page to confirm the privacy befoe pairing.""" 177 178 PAGE_RID = f'{constants.PKG_NAME_ID}/gdpr_scroll_view' 179 _NODE_ACCEPT_BTN_TEXT = 'ACCEPT' 180 _SWIPE_RETRY = 5 181 182 def accept(self) -> ui_core.UIPage: 183 """Accepts the privacy policy. 184 185 Returns: 186 The transformed page. 187 188 Raises: 189 errors.UIError: Fail to get target node. 190 """ 191 self.swipe_down() 192 for i in range(self._SWIPE_RETRY): 193 node = self.get_node_by_text(self._NODE_ACCEPT_BTN_TEXT) 194 if node is None: 195 raise errors.UIError( 196 f'Fail to find the node with text={self._NODE_ACCEPT_BTN_TEXT}') 197 198 atr_obj = node.attributes.get('enabled') 199 if atr_obj is not None and atr_obj.value == 'true': 200 return self.click_node_by_text(self._NODE_ACCEPT_BTN_TEXT) 201 202 self.log.debug('swipe down to browse the privacy info...%d', i + 1) 203 self.swipe_down() 204 self.ctx.get_page() 205 206 raise errors.UIError( 207 'Fail to wait for the enabled button to confirm the privacy!') 208 209 210class CancelPairPage(ui_core.UIPage): 211 """Fitbit Companion App's page to confirm the cancel of pairing.""" 212 213 PAGE_TEXT = ('Canceling this process may result in poor connectivity with ' 214 'your Fitbit device.') 215 NODE_YES_TEXT = 'YES' 216 NODE_NO_TEXT = 'NO' 217 218 def yes(self) -> ui_core.UIPage: 219 """Cancels the pairing process. 220 221 Returns: 222 The transformed page. 223 224 Raises: 225 errors.UIError: Fail to get target node. 226 """ 227 return self.click_node_by_text(self.NODE_YES_TEXT) 228 229 def no(self) -> ui_core.UIPage: 230 """Continues the pairing process. 231 232 Returns: 233 The transformed page. 234 235 Raises: 236 errors.UIError: Fail to get target node. 237 """ 238 return self.click_node_by_text(self.NODE_NO_TEXT) 239 240 241class CancelPair2Page(ui_core.UIPage): 242 """Fitbit Companion App's page to confirm the cancel of pairing.""" 243 244 PAGE_TEXT = ( 245 'Are you sure you want to cancel pairing?' 246 ' You can set up your Fitbit Device later on the Devices screen.') 247 248 _NODE_YES_TEXT = 'CANCEL' 249 250 def yes(self) -> ui_core.UIPage: 251 """Cancels the pairing process. 252 253 Returns: 254 The transformed page. 255 256 Raises: 257 errors.UIError: Fail to get target node. 258 """ 259 return self.click_node_by_text(self._NODE_YES_TEXT) 260 261 262class ConfirmReplaceSmartWatchPage(ui_core.UIPage): 263 """Fitbit Companion App's page to confirm the replacement of tracker device. 264 265 When you already have one paired tracker device and you try to pair a 266 new one, this page will show up. 267 """ 268 NODE_SWITCH_BTN_TEXT = 'SWITCH TO' 269 PAGE_TEXT = 'Switching?' 270 271 def confirm(self) -> ui_core.UIPage: 272 """Confirms the switching. 273 274 Returns: 275 The transformed page. 276 277 Raises: 278 errors.UIError: Fail to get target node. 279 """ 280 281 def _search_switch_btn_node(node: ui_node.UINode) -> bool: 282 if node.text.startswith(self.NODE_SWITCH_BTN_TEXT): 283 return True 284 285 return False 286 287 node = self.get_node_by_func(_search_switch_btn_node) 288 if node is None: 289 raise errors.UIError( 290 'Failed to confirm the switching of new tracker device!') 291 292 return self.click(node) 293 294 295class ConfirmChargePage(ui_core.UIPage): 296 """Fitbit Companion App's page to confirm the charge condition.""" 297 298 PAGE_RE_TEXT = 'Let your device charge during setup' 299 300 def next(self) -> ui_core.UIPage: 301 """Forwards to pairing page. 302 303 Returns: 304 The transformed page. 305 306 Raises: 307 errors.UIError: Fail to get target node. 308 """ 309 return self.click_node_by_text('NEXT') 310 311 312class ChooseTrackerPage(ui_core.UIPage): 313 """Fitbit Companion App's page to select device model for pairing.""" 314 315 ACTIVITY = f'{constants.PKG_NAME}/com.fitbit.device.ui.setup.choose.ChooseTrackerActivity' 316 PAGE_RID = f'{constants.PKG_NAME_ID}/choose_tracker_title_container' 317 318 def select_device(self, name: str) -> ui_core.UIPage: 319 """Selects tracker device. 320 321 Args: 322 name: The name of device. (e.g.: 'Buzz') 323 324 Returns: 325 The transformed page. 326 327 Raises: 328 errors.UIError: Fail to get target node. 329 """ 330 return self.click_node_by_text(name) 331 332 333class ConfirmDevicePage(ui_core.UIPage): 334 """Fitbit Companion App's page to confirm the selected tracker device.""" 335 PAGE_TEXT = 'SET UP' 336 ACTIVITY = f'{constants.PKG_NAME}/com.fitbit.device.ui.setup.choose.ConfirmDeviceActivity' 337 338 def confirm(self) -> ui_core.UIPage: 339 """Confirms the selection. 340 341 Returns: 342 The transformed page. 343 344 Raises: 345 errors.UIError: Fail to get target node. 346 """ 347 return self.click_node_by_text(self.PAGE_TEXT) 348 349 350class SkipInfoPage(ui_core.UIPage): 351 """Fitbit Companion App's page to skip the 'not working' page.""" 352 353 PAGE_TEXT = 'Skip Information Screens' 354 NODE_SKIP_TEXT = 'SKIP' 355 NODE_CONTINUE_TEXT = 'CONTINUE' 356 357 def skip(self) -> ui_core.UIPage: 358 """Skips the information screens. 359 360 Returns: 361 The transformed page. 362 """ 363 return self.click_node_by_text(self.NODE_SKIP_TEXT) 364 365 366class UpdateDevicePage(ui_core.UIPage): 367 """Fitbit Companion App's page to update device.""" 368 369 PAGE_TEXT = 'INSTALL UPDATE NOW' 370 NODE_UPDATE_LATER_BTN_TEXT = 'UPDATE LATER' 371 372 def update_later(self) -> ui_core.UIPage: 373 """Cancels the update. 374 375 Returns: 376 The transformed page. 377 """ 378 return self.click_node_by_text(self.NODE_UPDATE_LATER_BTN_TEXT) 379 380 381class PurchasePage(ui_core.UIPage): 382 """Fitbit Companion App's page to purchase merchandise.""" 383 384 PAGE_RE_TEXT = 'Protect Your New Device' 385 NODE_SKIP_BTN_TEXT = 'NOT NOW' 386 387 def skip(self) -> ui_core.UIPage: 388 """Skips the purchase action. 389 390 Returns: 391 The transformed page. 392 """ 393 return self.click_node_by_text(self.NODE_SKIP_BTN_TEXT) 394