#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""A script to open the unlock bootloader on-screen prompt on all devices."""

import argparse
import logging
import os
import subprocess
import sys
import time

if __name__ == '__main__':
  sys.path.append(
      os.path.abspath(os.path.join(os.path.dirname(__file__),
                                   '..', '..', '..')))

from devil import devil_env
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
from devil.android.sdk import fastboot
from devil.android.tools import script_common
from devil.utils import parallelizer


def reboot_into_bootloader(filter_devices):
  # Reboot all devices into bootloader if they aren't there already.
  rebooted_devices = set()
  for d in adb_wrapper.AdbWrapper.Devices(desired_state=None):
    if filter_devices and str(d) not in filter_devices:
      continue
    state = d.GetState()
    if state == 'device':
      logging.info('Booting %s to bootloader.', d)
      try:
        d.Reboot(to_bootloader=True)
        rebooted_devices.add(str(d))
      except (device_errors.AdbCommandFailedError,
              device_errors.DeviceUnreachableError):
        logging.exception('Unable to reboot device %s', d)
    else:
      logging.error('Unable to reboot device %s: %s', d, state)

  # Wait for the rebooted devices to show up in fastboot.
  if rebooted_devices:
    logging.info('Waiting for devices to reboot...')
    timeout = 60
    start = time.time()
    while True:
      time.sleep(5)
      fastbooted_devices = set([str(d) for d in fastboot.Fastboot.Devices()])
      if rebooted_devices <= set(fastbooted_devices):
        logging.info('All devices in fastboot.')
        break
      if time.time() - start > timeout:
        logging.error('Timed out waiting for %s to reboot.',
                      rebooted_devices - set(fastbooted_devices))
        break


def unlock_bootloader(d):
  # Unlock the phones.
  unlocking_processes = []
  logging.info('Unlocking %s...', d)
  # The command to unlock the bootloader could be either of the following
  # depending on the android version and/or oem. Can't really tell which is
  # needed, so just try both.
  # pylint: disable=protected-access
  cmd_old = [d._fastboot_path.read(), '-s', str(d), 'oem', 'unlock']
  cmd_new = [d._fastboot_path.read(), '-s', str(d), 'flashing', 'unlock']
  unlocking_processes.append(
      subprocess.Popen(
          cmd_old, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
  unlocking_processes.append(
      subprocess.Popen(
          cmd_new, stdout=subprocess.PIPE, stderr=subprocess.PIPE))

  # Give the unlocking command time to finish and/or open the on-screen prompt.
  logging.info('Sleeping for 5 seconds...')
  time.sleep(5)

  leftover_pids = []
  for p in unlocking_processes:
    p.poll()
    rc = p.returncode
    # If the command succesfully opened the unlock prompt on the screen, the
    # fastboot command process will hang and wait for a response. We still
    # need to read its stdout/stderr, so use os.read so that we don't
    # have to wait for EOF to be written.
    out = os.read(p.stderr.fileno(), 1024).strip().lower()
    if not rc:
      if out == '...' or out == '< waiting for device >':
        logging.info('Device %s is waiting for confirmation.', d)
      else:
        logging.error(
            'Device %s is hanging, but not waiting for confirmation: %s',
            d, out)
      leftover_pids.append(p.pid)
    else:
      if 'unknown command' in out:
        # Of the two unlocking commands, this was likely the wrong one.
        continue
      elif 'already unlocked' in out:
        logging.info('Device %s already unlocked.', d)
      elif 'unlock is not allowed' in out:
        logging.error("Device %s is oem locked. Can't unlock bootloader.", d)
        return 1
      else:
        logging.error('Device %s in unknown state: "%s"', d, out)
        return 1
    break

  if leftover_pids:
    logging.warning('Processes %s left over after unlocking.', leftover_pids)

  return 0


def main():
  logging.getLogger().setLevel(logging.INFO)

  parser = argparse.ArgumentParser()
  script_common.AddDeviceArguments(parser)
  parser.add_argument('--adb-path',
                      help='Absolute path to the adb binary to use.')
  args = parser.parse_args()

  devil_dynamic_config = devil_env.EmptyConfig()
  if args.adb_path:
    devil_dynamic_config['dependencies'].update(
        devil_env.LocalConfigItem(
            'adb', devil_env.GetPlatform(), args.adb_path))
  devil_env.config.Initialize(configs=[devil_dynamic_config])

  reboot_into_bootloader(args.devices)
  devices = [
      d for d in fastboot.Fastboot.Devices() if not args.devices or
          str(d) in args.devices]
  parallel_devices = parallelizer.Parallelizer(devices)
  parallel_devices.pMap(unlock_bootloader).pGet(None)
  return 0


if __name__ == '__main__':
  sys.exit(main())
