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