Exemplo n.º 1
0
    def bootstrap_mapper(self):
        mapper_config_path = os.path.join(self.config_manager.get_cfgpath(),
                                          "guiscrcpy.mapper.json")
        if os.path.exists(mapper_config_path):
            from guiscrcpy.lib.mapper.mapper import MapperAsync

            _, identifier = self.current_device_identifier()
            self.mp = MapperAsync(
                self,
                device_id=identifier,
                config_path=mapper_config_path,
                adb=self.adb,
                initialize=False,
            )
            self.mp.start()
            self.private_message_box_adb.setText(
                "guiscrcpy-mapper has started")
        else:
            message_box = QMessageBox()
            message_box.setText(
                "guiscrcpy mapper is not initialized yet. Do you want to "
                "initialize it now?")
            message_box.setInformativeText(
                "Before you initialize, make sure your phone is connected and "
                "the display is switched on to map the points.")
            message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            user_message_box_response = message_box.exec()
            values_devices_list = self.scan_devices_update_list_view()
            self.check_devices_status_and_select_first_if_only_one(
                values_devices_list=values_devices_list)
            # TODO: allow enabling mapper from inside
            if user_message_box_response == QMessageBox.Yes:
                self.private_message_box_adb.setText("Initializing mapper...")
                print("Make sure your phone is connected and display is "
                      "switched on")
                print("Reset mapper if you missed any "
                      "steps by 'guiscrcpy --mapper-reset'")
                print()
                print("If at first you don't succeed... "
                      "reset, reset and reset again! :D")
                print()
                _, identifier = self.current_device_identifier()
                executable = get_self()
                from .lib.utils import shellify as sx, open_process

                open_process(
                    sx("{} mapper".format(executable)),
                    stdout=sys.stdout,
                    stdin=sys.stdin,
                    stderr=sys.stderr,
                    cwd=os.getcwd(),
                )
                print("Mapper started")
                self.private_message_box_adb.setText("Mapper initialized")
Exemplo n.º 2
0
class InterfaceGuiscrcpy(QMainWindow, Ui_MainWindow):
    """
    Main class for guiscrcpy object.
    All the processes to spawn to scrcpy are handled here
    """

    # noinspection PyArgumentList
    def __init__(self,
                 cfgmgr,
                 adb,
                 scrcpy,
                 force_window_frame=False,
                 panels_not_always_on_top=False,
                 debug__no_scrcpy=False):
        QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)
        self._adb = adb
        self._scrcpy = scrcpy
        self.cfgmgr = cfgmgr
        config = self.cfgmgr.get_config()
        self._config = config
        self.panels_not_always_on_top = panels_not_always_on_top
        self.force_window_frame = force_window_frame
        self.debug__no_scrcpy = debug__no_scrcpy
        self.cmx = None
        self.sm = None
        self.mp = None
        self.nm = None
        self.swipe_instance = None
        self.panel_instance = None
        self.side_instance = None
        self.child_windows = list()
        self.options = ""
        log("Options received by class are : {} {} {} {} {} ".format(
            config['bitrate'],
            config['dimension'],
            config['swtouches'],
            config['dispRO'],
            config['fullscreen'],
        ))
        # ====================================================================
        # Rotation; read config, update UI
        self.device_rotation.setCurrentIndex(config.get("rotation", 0))
        self.dial.setValue(int(config['bitrate']))
        if config['swtouches']:
            self.showTouches.setChecked(True)
        else:
            self.showTouches.setChecked(False)
        if config['dispRO']:
            self.displayForceOn.setChecked(True)
        else:
            self.displayForceOn.setChecked(False)

        # panels
        if config['panels'].get('swipe'):
            self.check_swipe_panel.setChecked(True)
        else:
            self.check_swipe_panel.setChecked(False)
        if config['panels'].get('tookit'):
            self.check_side_panel.setChecked(True)
        else:
            self.check_side_panel.setChecked(False)
        if config['panels'].get('bottom'):
            self.check_bottom_panel.setChecked(True)
        else:
            self.check_bottom_panel.setChecked(False)

        # dimension
        if config['dimension'] is not None:
            self.dimensionDefaultCheckbox.setChecked(False)
            try:
                self.dimensionSlider.setValue(config['dimension'])
            except TypeError:
                self.dimensionDefaultCheckbox.setChecked(True)
        if config['fullscreen']:
            self.fullscreen.setChecked(True)
        else:
            self.fullscreen.setChecked(False)
        if is_running("scrcpy"):
            log("SCRCPY RUNNING")
            self.display_public_message("SCRCPY SERVER RUNNING")
        else:
            log("SCRCPY SERVER IS INACTIVE")
            self.display_public_message("SCRCPY SERVER NOT RUNNING")

        # CONNECT DIMENSION CHECK BOX TO STATE CHANGE
        self.dimensionDefaultCheckbox.stateChanged.connect(
            self.__dimension_change_cb)
        self.build_label.setText("Build {} by srevinsaju".format(VERSION))

        # DIAL CTRL GRP
        self.dial.sliderMoved.connect(self.__dial_change_cb)
        self.dial.sliderReleased.connect(self.__dial_change_cb)
        # DIAL CTRL GRP

        # MAIN EXECUTE ACTION
        self.executeaction.clicked.connect(self.start_act)
        try:
            if config['extra']:
                self.flaglineedit.setText(config['extra'])
        except Exception as err:
            log(f"Exception: flaglineedit.text(config[extra]) {err}")

        # set swipe instance, bottom instance and
        # side instance as enabled by default
        self.check_swipe_panel.setChecked(True)
        self.check_bottom_panel.setChecked(True)
        self.check_side_panel.setChecked(True)

        self.quit.clicked.connect(self.quit_window)
        self.dimensionText.setText("DEFAULT")
        config['bitrate'] = int(self.dial.value())
        self.bitrateText.setText(" " + str(config['bitrate']) + "KB/s")
        self.pushButton.setText("RESET")
        self.pushButton.clicked.connect(self.reset)
        self.abtgit.clicked.connect(self.launch_web_github)
        self.usbaud.clicked.connect(self.launch_usb_audio)
        self.initmapnow.clicked.connect(self.bootstrap_mapper)
        self.network_button.clicked.connect(self.network_mgr)
        self.settings_button.clicked.connect(self.settings_mgr)
        # self.devices_view.itemChanged.connect(self.update_rotation_combo_cb)
        self.devices_view.itemClicked.connect(self.update_rotation_combo_cb)
        self.refreshdevices.clicked.connect(self.scan_devices_update_list_view)
        self.restart_adb_server.clicked.connect(
            self.restart_adb_server_guiscrcpy)
        self.devices_view.itemClicked.connect(self.more_options_device_view)
        self.devices_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.scan_config_devices_update_list_view()
        self.refresh_devices()

    @property
    def adb(self):
        return self._adb

    @property
    def scrcpy(self):
        return self._scrcpy

    @property
    def config(self):
        return self._config

    def restart_adb_server_guiscrcpy(self):
        self.adb.kill_adb_server()

    def refresh_devices(self):
        """
        A slot for refreshing the QListView
        :return:
        """
        self.scan_devices_update_list_view()

    def update_rotation_combo_cb(self):
        """
        A proposed method for refreshing the rotation combobox on item
        change in the QListBox
        :return:
        """
        if self.devices_view.currentItem():
            _, device_id = self.current_device_identifier()
            _rotation = self.config.get('device').get(device_id, dict()).get(
                'rotation', self.device_rotation.currentIndex())
        else:
            _rotation = self.config.get("rotation",
                                        self.device_rotation.currentIndex())
        self.device_rotation.setCurrentIndex(_rotation)

    def settings_mgr(self):
        from guiscrcpy.ux.settings import InterfaceSettings
        self.sm = InterfaceSettings(self)
        self.sm.init()
        self.sm.show()

    def network_mgr(self):
        from guiscrcpy.ux.network import InterfaceNetwork
        self.nm = InterfaceNetwork(self.adb)
        self.nm.init()
        self.nm.show()

    def bootstrap_mapper(self):
        mapper_config_path = os.path.join(self.cfgmgr.get_cfgpath(),
                                          "guiscrcpy.mapper.json")
        if os.path.exists(mapper_config_path):
            from guiscrcpy.lib.mapper.mapper import MapperAsync
            _, identifier = self.current_device_identifier()
            self.mp = MapperAsync(self,
                                  device_id=identifier,
                                  config_path=mapper_config_path,
                                  adb=self.adb,
                                  initialize=False)
            self.mp.start()
            self.private_message_box_adb.setText(
                "guiscrcpy-mapper has started")
        else:
            message_box = QMessageBox()
            message_box.setText(
                "guiscrcpy mapper is not initialized yet. Do you want to "
                "initialize it now?")
            message_box.setInformativeText(
                "Before you initialize, make sure your phone is connected and "
                "the display is switched on to map the points.")
            message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            user_message_box_response = message_box.exec()
            values_devices_list = self.scan_devices_update_list_view()
            self.check_devices_status_and_select_first_if_only_one(
                values_devices_list=values_devices_list)
            # TODO: allow enabling mapper from inside
            if user_message_box_response == QMessageBox.Yes:
                self.private_message_box_adb.setText("Initializing mapper...")
                print("Make sure your phone is connected and display is "
                      "switched on")
                print("Reset mapper if you missed any "
                      "steps by 'guiscrcpy --mapper-reset'")
                print()
                print("If at first you don't succeed... "
                      "reset, reset and reset again! :D")
                print()
                _, identifier = self.current_device_identifier()
                executable = get_self()
                from .lib.utils import shellify as sx
                subprocess.Popen(sx('{} mapper'.format(executable)),
                                 stdout=sys.stdout,
                                 stdin=sys.stdin,
                                 stderr=sys.stderr,
                                 cwd=os.getcwd())
                print("Mapper started")
                self.private_message_box_adb.setText("Mapper initialized")

    @staticmethod
    def launch_usb_audio():
        for path in environment.paths():
            if os.path.exists(os.path.join(path, 'usbaudio')):
                path_to_usbaudio = os.path.join(path, 'usbaudio')
                break
        else:
            return
        Popen(path_to_usbaudio, stdout=PIPE, stderr=PIPE)

    @staticmethod
    def launch_web_github():
        webbrowser.open("https://github.com/srevinsaju/guiscrcpy")

    def about(self):
        """
        Reset message box is based on aboutWindow object
        For some reason, I did not get time to fix that
        :return:
        """
        about_message_box = QMessageBox().window()
        about_message_box.about(
            self.pushButton,
            "Info",
            "Please restart guiscrcpy to reset the settings. "
            "guiscrcpy will now exit",
        )
        about_message_box.addButton("OK", about_message_box.hide)  # noqa:
        about_message_box.show()

    def reset(self):
        """
        Remove configuration files; Reset the mapper and guiscrcpy.json
        :return:
        """
        self.cfgmgr.reset_config()
        log("CONFIGURATION FILE REMOVED SUCCESSFULLY")
        log("RESTART")
        message_box = QMessageBox().window()
        message_box.about(
            self.pushButton,
            "Info",
            "Please restart guiscrcpy to reset the settings. "
            "guiscrcpy will now exit",
        )
        QMessageBox.ButtonRole()
        message_box.addButton("OK", self.quit_window)  # noqa:
        message_box.show()

    def quit_window(self):
        """
        A method to quit the main window
        :return:
        """
        try:
            self.mp.exit()
        except (AttributeError, KeyboardInterrupt):
            pass
        sys.exit()

    def forget_paired_device(self):
        """
        Forgets / Removes the configuration for saved
        :return: popped item / False
        """
        try:
            _, identifier = self.current_device_identifier()
            popped_device = self.config['device'].pop(identifier)
            self.refresh_devices()
            self.cfgmgr.update_config(self.config)
            self.cfgmgr.write_file()
            return popped_device
        except KeyError:
            return False

    def more_options_device_view(self, button):
        if 'Disconnect' in button.text():
            menu = QMenu("Menu", self)
            menu.addAction("Pair / Ping", self.ping_paired_device)
            menu.addAction("Attempt TCPIP on device", self.tcpip_paired_device)
            menu.addAction("Forget device", self.forget_paired_device)
        else:
            menu = QMenu("Menu", self)
            menu.addAction("Attempt TCPIP on device", self.tcpip_paired_device)
            menu.addAction("Attempt reconnection", self.ping_paired_device)
            menu.addAction("Refresh", self.refresh_devices)
        _, identifier = self.current_device_identifier()
        if platform.System.system() == "Linux" and identifier.count('.') >= 3:
            menu.addAction("Add Desktop Shortcut to this device",
                           self.create_desktop_shortcut_linux_os)
        menu.exec_(
            self.devices_view.mapToGlobal(
                QPoint(
                    self.devices_view.visualItemRect(button).x() + 22,
                    self.devices_view.visualItemRect(button).y() + 22)))

    def create_desktop_shortcut_linux_os(self):
        """
        Creates a desktop shortcut for Linux OS
        :return:
        """
        # just a check before anything further happens because of an
        # unrelated OS
        if environment.system() != "Linux":
            log("Tried to run create_desktop_shortcut_linux_os on an "
                "unsupported OS.")
            return False

        # get device specific configuration
        model, identifier = self.current_device_identifier()
        picture_file_path = self.cfgmgr.get_cfgpath()
        __sha_shift = self.config.get('sha_shift', 5)
        sha = hashlib.sha256(
            str(identifier).encode()).hexdigest()[__sha_shift:__sha_shift + 6]
        log(f"Creating desktop shortcut sha: {sha}")
        path_to_image = os.path.join(picture_file_path, identifier + '.png')
        svg2png(bytestring=desktop_device_shortcut_svg().format(f"#{sha}"),
                write_to=path_to_image)

        # go through all args; break when we find guiscrcpy
        for args_i in range(len(sys.argv)):
            if 'guiscrcpy' in sys.argv[args_i]:
                aend = args_i + 1
                break
        else:
            aend = None

        sys_args_desktop = sys.argv[:aend]

        # check if its a python file
        # experimental support for AppImages / snaps
        # I am not sure; if it would work indeed
        for i in sys_args_desktop:
            if i.endswith('.py'):
                needs_python = True
                break
        else:
            needs_python = False
        if needs_python:
            sys_args_desktop = ['python3'] + sys_args_desktop

        # convert the list into a string
        sys_args_desktop = ' '.join(sys_args_desktop)
        auto_connect_run_command = \
            "{executable} --connect={ip} " \
            "--start --start-scrcpy-device-id={ip}".format(
                executable=sys_args_desktop,
                ip=identifier
            )

        # create the desktop file using linux's desktop file gen method
        path_to_desktop_file = platform.System().create_desktop(
            desktop_file=GUISCRCPY_DEVICE.format(
                identifier=model,
                command=auto_connect_run_command,
                icon_path=path_to_image),
            desktop_file_name=f'{model}.guiscrcpy.desktop')

        # announce it to developers / users
        log(f"Path to desktop file : {path_to_desktop_file}")
        print("Desktop file generated successfully")
        self.display_public_message("Desktop file has been created")

    def is_connection_success_handler(self, output: Popen, ip=None):
        out, err = output.communicate()
        if 'failed' in out.decode() or 'failed' in err.decode():
            self.display_public_message(
                "Failed to connect to {}. See the logs for more "
                "information".format(ip))
            print("adb:", out.decode(), err.decode())
        else:
            self.display_public_message(
                "Connection command completed successfully")

    def ping_paired_device(self, device_id=None):
        # update the configuration file first
        if not device_id:
            _, identifier = self.current_device_identifier()
            if identifier.count('.') == 3:
                wifi_device = True
            else:
                wifi_device = False
            try:
                self.config['device'][identifier]['wifi'] = wifi_device
            except KeyError:
                log(f"Failed writing the configuration "
                    f"'wifi' key to {identifier}")

            if wifi_device:
                ip = self.current_device_identifier()[1]
                output = self.adb.command('connect {}'.format(ip))
                self.is_connection_success_handler(output, ip=ip)
            else:
                self.adb.command('reconnect offline')
            # As we have attempted to connect; refresh the panel
            self.refresh_devices()
        else:
            output = self.adb.command('connect {}'.format(device_id))
            self.is_connection_success_handler(output, ip=device_id)

    def tcpip_paired_device(self):
        if self.devices_view.currentItem():
            _, identifier = self.current_device_identifier()
        else:
            identifier = ""
        __exit_code = self.adb.tcpip(identifier=identifier)
        if __exit_code != 0:
            self.display_public_message("TCP/IP failed on device. "
                                        "Please reconnect USB and try again")
        else:
            self.display_public_message("TCP/IP completed successfully.")
            time.sleep(0.1)  # wait for everything to get settled
            if identifier.count('.') >= 3:
                self.ping_paired_device(device_id=identifier)
            else:
                self.ping_paired_device()

    def current_device_identifier(self, need_status=False):
        if self.devices_view.currentItem():
            if need_status:
                return \
                    self.devices_view.currentItem().text().split()[0], \
                    self.devices_view.currentItem().text().split()[1], \
                    self.devices_view.currentItem().text().split()[2]
            else:
                return \
                    self.devices_view.currentItem().text().split()[0], \
                    self.devices_view.currentItem().text().split()[1]
        else:
            raise ValueError("No item is selected in QListView")

    def scan_config_devices_update_list_view(self):
        """
        Scans for saved devices
        :return:
        """
        self.devices_view.clear()
        paired_devices = self.config['device']
        for i in paired_devices:
            if paired_devices[i].get('wifi'):
                icon = ':/icons/icons/portrait_mobile_disconnect.svg'
                devices_view_list_item = QListWidgetItem(
                    QIcon(icon), "{device}\n{mode}\n{status}".format(
                        device=paired_devices[i].get('model'),
                        mode=i,
                        status='Disconnected'))
                __sha_shift = self.config.get('sha_shift', 5)
                __sha = hashlib.sha256(
                    str(i).encode()).hexdigest()[__sha_shift:__sha_shift + 6]
                devices_view_list_item.setToolTip(
                    "<span style='color: #{color}'>Device</snap>: <b>{d}</b>\n"
                    "Status: {s}".format(
                        d=i,
                        s="Disconnected. Right click 'ping' to attempt "
                        "reconnect",
                        color=__sha))
                devices_view_list_item.setFont(QFont('Noto Sans', 8))
                self.devices_view.addItem(devices_view_list_item)
        return paired_devices

    def scan_devices_update_list_view(self):
        """
        Scan for new devices; and update the list view
        :return:
        """
        # self.devices_view.clear()
        device_exists_in_view = False
        paired_devices = []
        for index in range(self.devices_view.count()):
            paired_devices.append(self.devices_view.item(index))

        __devices = self.adb.devices_detailed()
        log(__devices)
        for i in __devices:
            device_is_wifi = \
                i['identifier'].count('.') >= 3 and (':' in i['identifier'])

            if i['identifier'] not in self.config['device'].keys():
                device_paired_and_exists = False
                self.config['device'][i['identifier']] = {'rotation': 0}
            else:
                device_paired_and_exists = True

            if device_is_wifi:
                _icon_suffix = '_wifi'
            else:
                _icon_suffix = '_usb'

            icon = ':/icons/icons/portrait_mobile_white{}.svg'.format(
                _icon_suffix)

            if i['status'] == 'offline':
                icon = ':/icons/icons/portrait_mobile_error.svg'
            elif i['status'] == 'unauthorized':
                icon = ':/icons/icons/portrait_mobile_warning.svg'

            if i['status'] == 'no_permission':
                log("pairfilter: 5")
                # https://stackoverflow.com/questions/
                # 53887322/adb-devices-no-permissions-user-in-
                # plugdev-group-are-your-udev-rules-wrong
                udev_error = "Error connecting to device. Your udev rules are"\
                    " incorrect. See https://stackoverflow.com/questions"\
                    "/53887322/adb-devices-no-permissions-user-in-plugdev-"\
                    "group-are-your-udev-rules-wrong"
                self.display_public_message(udev_error)
                print(udev_error)
                return []
            # Check if device is unauthorized
            elif i['status'] == "unauthorized":
                log("unauthorized device detected: Click Allow on your device")
                log("pairfilter: 4")
                # The device is connected; and might/might't paired in the past
                # And is connected to the same IP address
                # It is possibly a bug with the connection;
                # Temporarily create a new QListItem to display the
                # device with the error
                paired = False
                device_paired_and_exists = False
                self.display_public_message(
                    f"{i['identifier']} is unauthorized. Please click allow "
                    f"on your device.")
                # Remove other devices with the same id and offline and
                # unauthorized
                self.remove_device_device_view(
                    i['identifier'], statuses=['offline', 'unauthorized'])
                # Unauthorized device cannot be considered as a paired device
                devices_view_list_item = QListWidgetItem()
            else:
                # check if device is paired
                # if yes, just update the list item
                if not device_paired_and_exists:
                    paired = False
                    devices_view_list_item = QListWidgetItem()
                else:
                    for paired_device in paired_devices:
                        if paired_device.text().split()[0] == i['model']:
                            log("pairfilter: 1")
                            paired = True
                            devices_view_list_item = paired_device
                            # as we have found a paired device
                            # we know by assumption; there cannot be two
                            # devices with the same local IP address;
                            # lets scan the devices_view once more in a loop
                            # to check for any device with the same
                            # identifier and remove them; based on this same
                            # assumption
                            self.remove_device_device_view(
                                i['identifier'],
                                statuses=['offline', 'unauthorized'])
                            break
                        elif paired_device.text().split()[1] ==\
                                i['identifier']:
                            log("pairfilter: 2")
                            self.remove_device_device_view(
                                i['identifier'],
                                statuses=['offline', 'unauthorized'])
                            devices_view_list_item = QListWidgetItem()
                            paired = False
                            break
                    else:
                        log("pairfilter: 3")
                        paired = False
                        devices_view_list_item = QListWidgetItem()

            devices_view_list_item.setIcon(QIcon(icon))

            devices_view_list_item.setText("{device}\n{mode}\n{status}".format(
                device=i['model'], mode=i['identifier'], status=i['status']))
            __sha_shift = self.config.get('sha_shift', 5)
            __sha = hashlib.sha256(str(i['identifier']).encode()).hexdigest(
            )[__sha_shift:__sha_shift + 6]
            devices_view_list_item.setToolTip(
                "Device: "
                "<span style='color: #{inv_color};background-color: #{color}'>"
                "<b>{d}</b></span>\n"
                "<br>"
                "Model: {m}\n<br>"
                "Alias: {a}\n<br>"
                "Status: {s}\n<br>"
                "Transport ID: {t}\n<br>"
                "Paired: {p}".format(d=i['identifier'],
                                     m=i['model'],
                                     a=i['product'],
                                     s=i['status'],
                                     t=i['transport_id'],
                                     p=paired,
                                     color=__sha,
                                     inv_color=str(
                                         hex(0xFFFFFF - int(__sha, 16))[2:])))

            devices_view_list_item.setFont(QFont('Noto Sans', 8))
            log(f"Pairing status: {device_paired_and_exists}")
            if device_paired_and_exists and device_is_wifi:
                # we need to only neglect wifi devices
                # paired usb device need to still show in the display
                continue
            elif device_exists_in_view:
                # devices exists in the list with the same status and
                # we should not add the new detected list item
                continue
            # If and only if the device doesn't exist; add it
            self.devices_view.addItem(devices_view_list_item)
        return __devices

    def remove_device_device_view(self, identifier: str = '', statuses=()):
        """
        Removes all QListWidgetItems from the device_view for all matching
        identifier
        :param identifier: str
        :param statuses: Iterable
        :return:
        """
        for index in range(self.devices_view.count() - 1, -1, -1):
            for status in statuses:
                if self.devices_view.item(index):
                    if str(identifier) in self.devices_view.item(index).text()\
                            and \
                            str(status) in \
                            self.devices_view.item(index).text():
                        self.devices_view.takeItem(index)
        return

    def __dimension_change_cb(self):
        if self.dimensionDefaultCheckbox.isChecked():
            self.dimensionSlider.setEnabled(False)
            self.config['dimension'] = None
            self.dimensionText.setInputMask("")
            self.dimensionText.setText("DEFAULT")
        else:
            self.dimensionSlider.setEnabled(True)
            self.config['dimension'] = int(self.dimensionSlider.value())
            self.dimensionText.setText(" " + str(self.config['dimension']) +
                                       "px")
            self.dimensionSlider.sliderMoved.connect(self.__slider_change_cb)
            self.dimensionSlider.sliderReleased.connect(
                self.__slider_change_cb)

    def __slider_change_cb(self):
        self.config['dimension'] = int(self.dimensionSlider.value())
        self.dimensionText.setText(str(self.config['dimension']) + "px")

    def __dial_change_cb(self):
        self.config['bitrate'] = int(self.dial.value())
        self.bitrateText.setText(str(self.config['bitrate']) + "KB/s")

    def progress(self, val):
        self.progressBar.setValue(val)
        if (val + 4) >= 100:
            return 100
        else:
            return val + 100 / 20

    @staticmethod
    def is_device_unusable(status):
        if any(('unauth' in status, 'offline' in status)):
            return True
        else:
            return False

    def show_device_status_failure(self, status):
        self.display_public_message(
            f"Device is {status}. Please reconnect / press allow.")

    def __reset_message_box_stylesheet(self):
        stylesheet = \
            "background-color: qlineargradient(" \
            "spread:pad, x1:0, y1:0, x2:1, y2:1, " \
            "stop:0 rgba(0, 255, 255, 255), " \
            "stop:1 rgba(0, 255, 152, 255)); " \
            "border-radius: 10px;"
        self.private_message_box_adb.setStyleSheet(stylesheet)

    def __select_first_device(self):
        self.devices_view.setCurrentIndex(
            QModelIndex(self.devices_view.model().index(0, 0)))

    def display_public_message(self, message):
        """
        sets the message box information (GUI) with the string message
        :param message: str
        :return: None
        """
        self.private_message_box_adb.setText(message)

    def check_devices_status_and_select_first_if_only_one(
            self, values_devices_list):
        """
        Checks the devices in the Grid View, and then checks if any device
        is available or offline accordingly display the error message. If
        only one device was detected, automatically select the first device
        and check its status of connectivity. Return the selected device if
        multiple devices were detected by adb. Return device_id, status and
        a bool more_devices if more than one device was found
        :param values_devices_list:
        :type values_devices_list:
        :return:
        :rtype:
        """
        if len(values_devices_list) == 0:
            # Could not detect any device
            self.display_public_message("Could not find any devices")
            return 0
        elif self.devices_view.currentIndex() is None and \
                len(values_devices_list) != 1:
            # No device is selected and more than one device found
            self.display_public_message("Please select a device below.")
            return 0
        else:
            # Device selected
            log("DEVICE LIST", len(values_devices_list), values_devices_list)
            if len(values_devices_list) == 1:
                # found only one device
                # ======================================================
                # Store the current rotation temporarily
                __selected_rotation = self.device_rotation.currentIndex()
                self.__select_first_device()
                # Restore the selected rotation
                self.device_rotation.setCurrentIndex(__selected_rotation)
                # =======================================================
                # get the status and identifier of the device;
                # return if device is not in a connectable state
                try:
                    _, device_id, _stat = \
                        self.current_device_identifier(need_status=True)
                    if self.is_device_unusable(_stat):
                        self.show_device_status_failure(_stat)
                        return 0
                except ValueError:
                    self.display_public_message(
                        "Please select a device from the list view")
                    return 0
                more_devices = False
            elif self.devices_view.currentItem() is None:
                # no item is selected
                self.display_public_message("Please select a device below.")
                return 0
            else:
                _, device_id, _stat = self.current_device_identifier(
                    need_status=True)
                if self.is_device_unusable(_stat):
                    self.show_device_status_failure(_stat)
                    return 0
                log("Device_id = {}".format(device_id))
                more_devices = True
        return device_id, more_devices, _stat

    def start_act(self):
        """
        Main brain of guiscrcpy; handles what to do when
        :return:
        """
        # prepare launch of scrcpy,
        # reset colors
        # reset vars

        # 1: reset
        self.options = ""
        progress = self.progress(0)
        self.__reset_message_box_stylesheet()

        # ====================================================================
        # 2: Update UI to start checking
        self.display_public_message("CHECKING DEVICE CONNECTION")
        initial_time = time.time()
        progress = self.progress(progress)

        # ====================================================================
        # 3: Check devices
        values_devices_list = self.scan_devices_update_list_view()
        e = self.check_devices_status_and_select_first_if_only_one(
            values_devices_list)
        if e is None or isinstance(e, int):
            return e
        device_id, more_devices, _stat = e
        progress = self.progress(progress)

        # ====================================================================
        # 4: Parse dimension slider
        # check if the defaultDimension is checked or not for giving signal
        if self.dimensionDefaultCheckbox.isChecked():
            self.dimensionSlider.setEnabled(False)
            self.dimensionText.setText("DEFAULT")
            self.config['dimension'] = None
        else:
            self.dimensionSlider.setEnabled(True)
            self.config['dimension'] = int(self.dimensionSlider.value())
            self.dimensionSlider.setValue(self.config['dimension'])
            self.dimensionText.setText(str(self.config['dimension']) + "px")
        # edit configuration files to update dimension key
        if self.config['dimension'] is None:
            self.options = " "
        elif self.config['dimension'] is not None:
            self.options = " -m " + str(self.config['dimension'])
        else:
            self.options = ""
        progress = self.progress(progress)

        # ====================================================================
        # 5: Check if always_on and fullscreen switches are on
        if self.aotop.isChecked():
            self.options += " --always-on-top"
        if self.fullscreen.isChecked():
            self.options += " -f"
            self.config['fullscreen'] = True
        else:
            self.config['fullscreen'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 6: Check if show touches / recording are on
        if self.showTouches.isChecked():
            self.options += " --show-touches"
            self.config['swtouches'] = True
        else:
            self.config['swtouches'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 7: Check if the record option is selected
        if self.recScui.isChecked():
            self.options += " -r " + str(int(time.time())) + ".mp4 "
        progress = self.progress(progress)

        # ====================================================================
        # 8: Check if the display is forced to be on
        if self.displayForceOn.isChecked():
            self.options += " -S"
            self.config['dispRO'] = True
        else:
            self.config['dispRO'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 9: Parse bitrate
        # Bitrate is parsed, by editing the bitrate mask
        if self.bitrateText.text().split()[1][0] in ['K', 'M', 'T']:
            bitrate_multiplier = str(self.bitrateText.text().split()[1][0])
        elif self.bitrateText.text().split()[1][0] == "B":
            bitrate_multiplier = "B"
        else:
            # do not proceed. Invalid file size multiplier
            multiplier_error = f"Invalid file size multiplier \
            '{str(self.bitrateText.text().split()[1][0])}'. " \
                                               f"Please use only K, M, T only"
            print(multiplier_error)
            self.display_public_message(multiplier_error)
            return False
        if self.bitrateText.text().split()[0].isdigit():
            bitrate_integer = int(self.bitrateText.text().split()[0])
        else:
            bitrate_integer = 8000
        self.options += " -b {}{}".format(bitrate_integer, bitrate_multiplier)
        self.config['bitrate'] = bitrate_integer
        progress = self.progress(progress)

        # ====================================================================
        # 10: Make user aware that there were no problems in connection
        # or in the data provided by the user
        log("CONNECTION ESTABLISHED")
        self.progressBar.setValue(50)
        log("Flags passed to scrcpy engine : " + self.options)
        self.progressBar.setValue(60)
        self.config['extra'] = self.flaglineedit.text()
        progress = self.progress(progress)

        # ====================================================================
        # 11: Initialize User Experience Mapper
        ux = UXMapper(adb=self.adb,
                      device_id=device_id,
                      sha_shift=self.config.get('sha_shift', 5))
        progress = self.progress(progress)
        always_on_top = \
            self.config.get('panels_always_on_top', False) or \
            not self.panels_not_always_on_top
        # ====================================================================
        # 12: Init side_panel if necessary
        if self.check_side_panel.isChecked():
            self.config['panels']['toolkit'] = True
            side_instance = InterfaceToolkit(parent=self,
                                             ux_mapper=ux,
                                             frame=self.force_window_frame,
                                             always_on_top=always_on_top)
            for instance in self.child_windows:
                if instance.ux.get_sha() == side_instance.ux.get_sha() and \
                        instance.name == side_instance.name and \
                        not instance.isHidden():
                    break
            else:
                side_instance.init()
                self.child_windows.append(side_instance)
        else:
            self.config['panels']['toolkit'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 13: Init bottom_panel if necessary
        if self.check_bottom_panel.isChecked():
            self.config['panels']['bottom'] = True
            panel_instance = Panel(parent=self,
                                   ux_mapper=ux,
                                   frame=self.force_window_frame,
                                   always_on_top=always_on_top)
            for instance in self.child_windows:
                if instance.ux.get_sha() == panel_instance.ux.get_sha() and \
                        instance.name == panel_instance.name and \
                        not instance.isHidden():
                    break
            else:
                panel_instance.init()
                self.child_windows.append(panel_instance)
        else:
            self.config['panels']['bottom'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 14: Init swipe panel if necessary
        if self.check_swipe_panel.isChecked():
            self.config['panels']['swipe'] = True
            swipe_instance = SwipeUX(
                ux_wrapper=ux,
                frame=self.force_window_frame,
                always_on_top=always_on_top)  # Load swipe UI
            for instance in self.child_windows:
                if instance.ux.get_sha() == swipe_instance.ux.get_sha() and \
                        instance.name == swipe_instance.name and \
                        not instance.isHidden():
                    break
            else:
                swipe_instance.init()
                self.child_windows.append(swipe_instance)
        else:
            self.config['panels']['swipe'] = False
        progress = self.progress(progress)

        # ====================================================================
        # 15: Generate uuid for device and set uuid color for PMBA
        hexdigest = ux.get_sha()[:6]
        stylesheet = f"background-color: #{hexdigest}; border-radius: 10px; "
        self.private_message_box_adb.setStyleSheet(stylesheet)
        progress = self.progress(progress)

        # ====================================================================
        # 16: Update device specific configuration
        model, identifier = self.current_device_identifier()
        # ====================================================================
        # 17: Parse rotation (scrcpy v1.13+)
        rotation_index = self.device_rotation.currentIndex() - 1
        if self.lock_rotation.isChecked():
            rotation_parameter = "--lock-video-orientation"
        else:
            rotation_parameter = "--rotation"
        if rotation_index != -1:
            self.options += " {} {}".format(rotation_parameter, rotation_index)
            self.config['device'][identifier]['rotation'] = \
                rotation_index + 1
        else:
            self.config['device'][identifier]['rotation'] = 0

        # ====================================================================
        # 18: Update device specific configuration
        if identifier.count('.') >= 3 and identifier[-1].isdigit():
            self.config['device'][identifier]['wifi'] = True
            self.config['device'][identifier]['model'] = model

        # ====================================================================
        # 16: Parse scrcpy arguments
        if self.cmx is not None:
            self.config['cmx'] = ' '.join(map(str, self.cmx))

        arguments_scrcpy = "{} {} {}".format(self.options,
                                             self.config['extra'],
                                             self.config['cmx'])
        progress = self.progress(progress)

        # ====================================================================
        # 18: Handle more devices
        if more_devices:
            # guiscrcpy found more devices
            # scrcpy will fail if more than one device is found
            # its important to pass the device serial id, if more than one
            # device is found
            arguments_scrcpy = f"-s {device_id} {arguments_scrcpy}"

        # tell end users that the color of the device is this
        self.display_public_message(
            f"Device {device_id} is connected; (color id matches "
            f"toolkit color)")
        log("Device connection completed successfully.")
        log("Private message box updated successfully")
        progress = self.progress(progress)

        # ====================================================================
        # 19: spawn scrcpy
        if not self.debug__no_scrcpy:
            # for debugging purposes, its important to not start scrcpy
            # every time
            self.scrcpy.start(arguments_scrcpy,
                              stdout=sys.stdout,
                              stderr=sys.stderr)
        progress = self.progress(progress)

        # ====================================================================
        # 20: Calculate time
        final_time = time.time()
        eta = final_time - initial_time
        print("scrcpy launched in {:.2f}s".format(eta))
        progress = self.progress(progress)

        # ====================================================================
        # 22: Update configuration
        self.cfgmgr.update_config(self.config)
        self.cfgmgr.write_file()
        progress = self.progress(progress)

        return self.progress(progress)