1#  Copyright (C) 2024 The Android Open Source Project
2#
3#  Licensed under the Apache License, Version 2.0 (the "License");
4#  you may not use this file except in compliance with the License.
5#  You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#  Unless required by applicable law or agreed to in writing, software
10#  distributed under the License is distributed on an "AS IS" BASIS,
11#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#  See the License for the specific language governing permissions and
13#  limitations under the License.
14
15"""Android Nearby device setup."""
16
17import base64
18import dataclasses
19import datetime
20import time
21from typing import Mapping
22
23from mobly.controllers import android_device
24from mobly.controllers.android_device_lib import adb
25
26from betocq import gms_auto_updates_util
27from betocq import nc_constants
28
29WIFI_COUNTRYCODE_CONFIG_TIME_SEC = 3
30TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC = 2
31PH_FLAG_WRITE_WAIT_TIME_SEC = 3
32WIFI_DISCONNECTION_DELAY_SEC = 3
33ADB_RETRY_WAIT_TIME_SEC = 2
34
35_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC = 2
36
37
38read_ph_flag_failed = False
39
40LOG_TAGS = [
41    'Nearby',
42    'NearbyMessages',
43    'NearbyDiscovery',
44    'NearbyConnections',
45    'NearbyMediums',
46    'NearbySetup',
47]
48
49
50def set_country_code(
51    ad: android_device.AndroidDevice, country_code: str
52) -> None:
53  """Sets Wi-Fi and Telephony country code.
54
55  When you set the phone to EU or JP, the available 5GHz channels shrinks.
56  Some phones, like Pixel 2, can't use Wi-Fi Direct or Hotspot on 5GHz
57  in these countries. Pixel 3+ can, but only on some channels.
58  Not all of them. So, test Nearby Share or Nearby Connections without
59  Wi-Fi LAN to catch any bugs and make sure we don't break it later.
60
61  Args:
62    ad: AndroidDevice, Mobly Android Device.
63    country_code: WiFi and Telephony Country Code.
64  """
65  try:
66    _do_set_country_code(ad, country_code)
67  except adb.AdbError:
68    ad.log.exception(
69        f'Failed to set country code on device "{ad.serial}, try again.'
70    )
71    time.sleep(ADB_RETRY_WAIT_TIME_SEC)
72    _do_set_country_code(ad, country_code)
73
74
75def _do_set_country_code(
76    ad: android_device.AndroidDevice, country_code: str
77) -> None:
78  """Sets Wi-Fi and Telephony country code."""
79  if not ad.is_adb_root:
80    ad.log.info(
81        f'Skipped setting wifi country code on device "{ad.serial}" '
82        'because we do not set country code on unrooted phone.'
83    )
84    return
85
86  ad.log.info(f'Set Wi-Fi and Telephony country code to {country_code}.')
87  ad.adb.shell('cmd wifi set-wifi-enabled disabled')
88  time.sleep(WIFI_COUNTRYCODE_CONFIG_TIME_SEC)
89  ad.adb.shell(
90      'am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE'
91      f' --es country {country_code}'
92  )
93  ad.adb.shell(f'cmd wifi force-country-code enabled {country_code}')
94  enable_airplane_mode(ad)
95  time.sleep(WIFI_COUNTRYCODE_CONFIG_TIME_SEC)
96  disable_airplane_mode(ad)
97  ad.adb.shell('cmd wifi set-wifi-enabled enabled')
98  telephony_country_code = (
99      ad.adb.shell('dumpsys wifi | grep mTelephonyCountryCode')
100      .decode('utf-8')
101      .strip()
102  )
103  ad.log.info(f'Telephony country code: {telephony_country_code}')
104
105
106def enable_logs(ad: android_device.AndroidDevice) -> None:
107  """Enables Nearby related logs."""
108  ad.log.info('Enable Nearby loggings.')
109  for tag in LOG_TAGS:
110    ad.adb.shell(f'setprop log.tag.{tag} VERBOSE')
111
112
113def grant_manage_external_storage_permission(
114    ad: android_device.AndroidDevice, package_name: str
115) -> None:
116  """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet."""
117  try:
118    _do_grant_manage_external_storage_permission(ad, package_name)
119  except adb.AdbError:
120    ad.log.exception(
121        'Failed to grant MANAGE_EXTERNAL_STORAGE permission on device'
122        f' "{ad.serial}", try again.'
123    )
124    time.sleep(ADB_RETRY_WAIT_TIME_SEC)
125    _do_grant_manage_external_storage_permission(ad, package_name)
126
127
128def _do_grant_manage_external_storage_permission(
129    ad: android_device.AndroidDevice, package_name: str
130) -> None:
131  """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet."""
132  build_version_sdk = int(ad.build_info['build_version_sdk'])
133  if build_version_sdk < 30:
134    return
135  ad.log.info(
136      f'Grant MANAGE_EXTERNAL_STORAGE permission on device "{ad.serial}".'
137  )
138  _grant_manage_external_storage_permission(ad, package_name)
139
140
141def dump_gms_version(ad: android_device.AndroidDevice) -> Mapping[str, str]:
142  """Dumps GMS version from dumpsys to sponge properties."""
143  try:
144    gms_version = _do_dump_gms_version(ad)
145  except adb.AdbError:
146    ad.log.exception(
147        f'Failed to dump GMS version on device "{ad.serial}", try again.'
148    )
149    time.sleep(ADB_RETRY_WAIT_TIME_SEC)
150    gms_version = _do_dump_gms_version(ad)
151  return gms_version
152
153
154def _do_dump_gms_version(ad: android_device.AndroidDevice) -> Mapping[str, str]:
155  """Dumps GMS version from dumpsys to sponge properties."""
156  out = (
157      ad.adb.shell(
158          'dumpsys package com.google.android.gms | grep "versionCode="'
159      )
160      .decode('utf-8')
161      .strip()
162  )
163  return {f'GMS core version on {ad.serial}': out}
164
165
166def toggle_airplane_mode(ad: android_device.AndroidDevice) -> None:
167  """Toggles airplane mode on the given device."""
168  ad.log.info('turn on airplane mode')
169  enable_airplane_mode(ad)
170  ad.log.info('turn off airplane mode')
171  disable_airplane_mode(ad)
172
173
174def connect_to_wifi_sta_till_success(
175    ad: android_device.AndroidDevice, wifi_ssid: str, wifi_password: str
176) -> datetime.timedelta:
177  """Connecting to the specified wifi STA/AP."""
178  ad.log.info('Start connecting to wifi STA/AP')
179  wifi_connect_start = datetime.datetime.now()
180  if not wifi_password:
181    wifi_password = None
182  connect_to_wifi(ad, wifi_ssid, wifi_password)
183  return datetime.datetime.now() - wifi_connect_start
184
185
186def connect_to_wifi(
187    ad: android_device.AndroidDevice,
188    ssid: str,
189    password: str | None = None,
190) -> None:
191  if not ad.nearby.wifiIsEnabled():
192    ad.nearby.wifiEnable()
193  # return until the wifi is connected.
194  password = password or None
195  ad.log.info('Connect to wifi: ssid: %s, password: %s', ssid, password)
196  ad.nearby.wifiConnectSimple(ssid, password)
197
198
199def disconnect_from_wifi(ad: android_device.AndroidDevice) -> None:
200  if not ad.is_adb_root:
201    ad.log.info("Can't clear wifi network in non-rooted device")
202    return
203  ad.nearby.wifiClearConfiguredNetworks()
204  time.sleep(WIFI_DISCONNECTION_DELAY_SEC)
205
206
207def _grant_manage_external_storage_permission(
208    ad: android_device.AndroidDevice, package_name: str
209) -> None:
210  """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet.
211
212  This permission will not grant automatically by '-g' option of adb install,
213  you can check the all permission granted by:
214  am start -a android.settings.APPLICATION_DETAILS_SETTINGS
215           -d package:{YOUR_PACKAGE}
216
217  Reference for MANAGE_EXTERNAL_STORAGE:
218  https://developer.android.com/training/data-storage/manage-all-files
219
220  This permission will reset to default "Allow access to media only" after
221  reboot if you never grant "Allow management of all files" through system UI.
222  The appops command and MANAGE_EXTERNAL_STORAGE only available on API 30+.
223
224  Args:
225    ad: AndroidDevice, Mobly Android Device.
226    package_name: The nearbu snippet package name.
227  """
228  try:
229    ad.adb.shell(
230        f'appops set --uid {package_name} MANAGE_EXTERNAL_STORAGE allow'
231    )
232  except adb.Error:
233    ad.log.info('Failed to grant MANAGE_EXTERNAL_STORAGE permission.')
234
235
236def enable_airplane_mode(ad: android_device.AndroidDevice) -> None:
237  """Enables airplane mode on the given device."""
238  try:
239    _do_enable_airplane_mode(ad)
240  except adb.AdbError:
241    ad.log.exception(
242        f'Failed to enable airplane mode on device "{ad.serial}", try again.'
243    )
244    time.sleep(ADB_RETRY_WAIT_TIME_SEC)
245    _do_enable_airplane_mode(ad)
246
247
248def _do_enable_airplane_mode(ad: android_device.AndroidDevice) -> None:
249  if (ad.is_adb_root):
250    ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '1'])
251    ad.adb.shell([
252        'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
253        'state', 'true'
254    ])
255  ad.adb.shell(['svc', 'wifi', 'disable'])
256  ad.adb.shell(['svc', 'bluetooth', 'disable'])
257  time.sleep(TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC)
258
259
260def disable_airplane_mode(ad: android_device.AndroidDevice) -> None:
261  """Disables airplane mode on the given device."""
262  try:
263    _do_disable_airplane_mode(ad)
264  except adb.AdbError:
265    ad.log.exception(
266        f'Failed to disable airplane mode on device "{ad.serial}", try again.'
267    )
268    time.sleep(ADB_RETRY_WAIT_TIME_SEC)
269    _do_disable_airplane_mode(ad)
270
271
272def _do_disable_airplane_mode(ad: android_device.AndroidDevice) -> None:
273  if (ad.is_adb_root):
274    ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '0'])
275    ad.adb.shell([
276        'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
277        'state', 'false'
278    ])
279  ad.adb.shell(['svc', 'wifi', 'enable'])
280  ad.adb.shell(['svc', 'bluetooth', 'enable'])
281  time.sleep(TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC)
282
283
284def check_if_ph_flag_committed(
285    ad: android_device.AndroidDevice,
286    pname: str,
287    flag_name: str,
288) -> bool:
289  """Check if P/H flag is committed.
290
291  Some devices don't support to check the flag with sqlite3. After the flag
292  check fails for the first time, it won't try it again.
293
294  Args:
295    ad: AndroidDevice, Mobly Android Device.
296    pname: The package name of the P/H flag.
297    flag_name: The name of the P/H flag.
298
299  Returns:
300    True if the P/H flag is committed.
301  """
302  global read_ph_flag_failed
303  if read_ph_flag_failed:
304    return False
305  sql_str = (
306      'sqlite3 /data/data/com.google.android.gms/databases/phenotype.db'
307      ' "select name, quote(coalesce(intVal, boolVal, floatVal, stringVal,'
308      ' extensionVal)) from FlagOverrides where committed=1 AND'
309      f' packageName=\'{pname}\';"'
310  )
311  try:
312    flag_result = ad.adb.shell(sql_str).decode('utf-8').strip()
313    return flag_name in flag_result
314  except adb.AdbError:
315    read_ph_flag_failed = True
316    ad.log.exception('Failed to check PH flag')
317  return False
318
319
320def write_ph_flag(
321    ad: android_device.AndroidDevice,
322    pname: str,
323    flag_name: str,
324    flag_type: str,
325    flag_value: str,
326) -> None:
327  """Write P/H flag."""
328  ad.adb.shell(
329      'am broadcast -a "com.google.android.gms.phenotype.FLAG_OVERRIDE" '
330      f'--es package "{pname}" --es user "*" '
331      f'--esa flags "{flag_name}" '
332      f'--esa types "{flag_type}" --esa values "{flag_value}" '
333      'com.google.android.gms'
334  )
335  time.sleep(PH_FLAG_WRITE_WAIT_TIME_SEC)
336
337
338def check_and_try_to_write_ph_flag(
339    ad: android_device.AndroidDevice,
340    pname: str,
341    flag_name: str,
342    flag_type: str,
343    flag_value: str,
344) -> None:
345  """Check and try to enable the given flag on the given device."""
346  if(not ad.is_adb_root):
347    ad.log.info(
348        "Can't read or write P/H flag value in non-rooted device. Use Mobile"
349        ' Utility app to config instead.'
350    )
351    return
352
353  if check_if_ph_flag_committed(ad, pname, flag_name):
354    ad.log.info(f'{flag_name} is already committed.')
355    return
356  ad.log.info(f'write {flag_name}.')
357  write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
358
359  if check_if_ph_flag_committed(ad, pname, flag_name):
360    ad.log.info(f'{flag_name} is configured successfully.')
361  else:
362    ad.log.info(f'failed to configure {flag_name}.')
363
364
365def enable_bluetooth_multiplex(ad: android_device.AndroidDevice) -> None:
366  """Enable bluetooth multiplex on the given device."""
367  pname = 'com.google.android.gms.nearby'
368  flag_name = 'mediums_supports_bluetooth_multiplex_socket'
369  flag_type = 'boolean'
370  flag_value = 'true'
371  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
372
373
374def enable_wifi_aware(ad: android_device.AndroidDevice) -> None:
375  """Enable wifi aware on the given device."""
376  pname = 'com.google.android.gms.nearby'
377  flag_name = 'mediums_supports_wifi_aware'
378  flag_type = 'boolean'
379  flag_value = 'true'
380
381  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
382
383
384def enable_dfs_scc(ad: android_device.AndroidDevice) -> None:
385  """Enable WFD/WIFI_HOTSPOT in a STA-associated DFS channel."""
386  pname = 'com.google.android.gms.nearby'
387  flag_name = 'mediums_lower_dfs_channel_priority'
388  flag_type = 'boolean'
389  flag_value = 'false'
390
391  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
392
393
394def disable_wlan_deny_list(ad: android_device.AndroidDevice) -> None:
395  """Enable WFD/WIFI_HOTSPOT in a STA-associated DFS channel."""
396  pname = 'com.google.android.gms.nearby'
397  flag_name = 'wifi_lan_blacklist_verify_bssid_interval_hours'
398  flag_type = 'long'
399  flag_value = '0'
400
401  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
402
403  flag_name = 'mediums_wifi_lan_temporary_blacklist_verify_bssid_interval_hours'
404  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
405
406
407def enable_ble_scan_throttling_during_2g_transfer(
408    ad: android_device.AndroidDevice, enable_ble_scan_throttling: bool = False
409) -> None:
410  """Enable BLE scan throttling during 2G transfer.
411  """
412
413  # The default values for the following parameters are 3 mins which are long
414  # enough for the performance test.
415  # mediums_ble_client_wifi_24_ghz_warming_up_duration
416  # fast_pair_wifi_24_ghz_warming_up_duration
417  # sharing_wifi_24_ghz_warming_up_duration
418
419  pname = 'com.google.android.gms.nearby'
420  flag_name = 'fast_pair_enable_connection_state_changed_listener'
421  flag_type = 'boolean'
422  flag_value = 'true' if enable_ble_scan_throttling else 'false'
423  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
424
425  flag_name = 'sharing_enable_connection_state_changed_listener'
426  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
427
428  flag_name = 'mediums_ble_client_enable_connection_state_changed_listener'
429  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
430
431
432def disable_redaction(ad: android_device.AndroidDevice) -> None:
433  """Disable info log redaction on the given device."""
434  pname = 'com.google.android.gms'
435  flag_name = 'ClientLogging__enable_info_log_redaction'
436  flag_type = 'boolean'
437  flag_value = 'false'
438
439  check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
440
441
442def install_apk(ad: android_device.AndroidDevice, apk_path: str) -> None:
443  """Installs the apk on the given device."""
444  ad.adb.install(['-r', '-g', '-t', apk_path])
445
446
447def disable_gms_auto_updates(ad: android_device.AndroidDevice) -> None:
448  """Disable GMS auto updates on the given device."""
449  if not ad.is_adb_root:
450    ad.log.warning(
451        'You should disable the play store auto updates manually on a'
452        'unrooted device, otherwise the test may be broken unexpected')
453  ad.log.info('try to disable GMS Auto Updates.')
454  gms_auto_updates_util.GmsAutoUpdatesUtil(ad).disable_gms_auto_updates()
455  time.sleep(_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC)
456
457
458def enable_gms_auto_updates(ad: android_device.AndroidDevice) -> None:
459  """Enable GMS auto updates on the given device."""
460  if not ad.is_adb_root:
461    ad.log.warning(
462        'You may enable the play store auto updates manually on a'
463        'unrooted device after test.')
464  ad.log.info('try to enable GMS Auto Updates.')
465  gms_auto_updates_util.GmsAutoUpdatesUtil(ad).enable_gms_auto_updates()
466  time.sleep(_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC)
467
468
469def get_wifi_sta_frequency(ad: android_device.AndroidDevice) -> int:
470  """Get wifi STA frequency on the given device."""
471  wifi_sta_status = dump_wifi_sta_status(ad)
472  if not wifi_sta_status:
473    return nc_constants.INVALID_INT
474  prefix = 'Frequency:'
475  postfix = 'MHz'
476  return get_int_between_prefix_postfix(wifi_sta_status, prefix, postfix)
477
478
479def get_wifi_p2p_frequency(ad: android_device.AndroidDevice) -> int:
480  """Get wifi p2p frequency on the given device."""
481  wifi_p2p_status = dump_wifi_p2p_status(ad)
482  if not wifi_p2p_status:
483    return nc_constants.INVALID_INT
484  prefix = 'channelFrequency='
485  postfix = ', groupRole=GroupOwner'
486  return get_int_between_prefix_postfix(wifi_p2p_status, prefix, postfix)
487
488
489def get_wifi_sta_max_link_speed(ad: android_device.AndroidDevice) -> int:
490  """Get wifi STA max supported Tx link speed on the given device."""
491  wifi_sta_status = dump_wifi_sta_status(ad)
492  if not wifi_sta_status:
493    return nc_constants.INVALID_INT
494  prefix = 'Max Supported Tx Link speed:'
495  postfix = 'Mbps'
496  return get_int_between_prefix_postfix(wifi_sta_status, prefix, postfix)
497
498
499def get_int_between_prefix_postfix(
500    string: str, prefix: str, postfix: str
501) -> int:
502  left_index = string.rfind(prefix)
503  right_index = string.rfind(postfix)
504  if left_index > 0 and right_index > left_index:
505    try:
506      return int(string[left_index + len(prefix): right_index].strip())
507    except ValueError:
508      return nc_constants.INVALID_INT
509  return nc_constants.INVALID_INT
510
511
512def dump_wifi_sta_status(ad: android_device.AndroidDevice) -> str:
513  """Dumps wifi STA status on the given device."""
514  try:
515    return (
516        ad.adb.shell('cmd wifi status | grep WifiInfo').decode('utf-8').strip()
517    )
518  except adb.AdbError:
519    return ''
520
521
522def dump_wifi_p2p_status(ad: android_device.AndroidDevice) -> str:
523  """Dumps wifi p2p status on the given device."""
524  try:
525    return (
526        ad.adb.shell('dumpsys wifip2p').decode('utf-8').strip()
527    )
528  except adb.AdbError:
529    return ''
530
531
532def get_hardware(ad: android_device.AndroidDevice) -> str:
533  """Gets hardware information on the given device."""
534  return ad.adb.getprop('ro.hardware')
535