#!/bin/sh
# tlp-func-usb - USB Functions
#
# Copyright (c) 2022 Thomas Koch <linrunner at gmx.net> and others.
# This software is licensed under the GPL v2 or later.

# Needs: tlp-func-base

# shellcheck disable=SC2086,SC2155

# ----------------------------------------------------------------------------
# Constants

readonly USBD=/sys/bus/usb/devices
readonly USB_TIMEOUT_MS=2000
readonly USB_WWAN_VENDORS="0bdb 05c6 1199 2cb7"
readonly USB_DONE=usb_done

# ----------------------------------------------------------------------------
# Functions

# --- USB Autosuspend

usb_suspend_device () {
    # enable/disable usb autosuspend for a single device
    # except input, scanners and denylisted
    # $1: device syspath
    # $2: batch/udev
    # $3: auto=enable/on=disable
    local usbdev=$1

    if [ -f $usbdev/power/autosuspend_delay_ms ]; then
        # device is autosuspendable
        local vendor="$(read_sysf $usbdev/idVendor)"
        local usbid="$vendor:$(read_sysf $usbdev/idProduct)"
        local busdev="Bus $(read_sysf $usbdev/busnum) Dev $(read_sysf $usbdev/devnum)"
        local dclass="$(read_sysf $usbdev/bDeviceClass)"
        local control="${3:-auto}"
        local caller="$2"
        local exc=""
        local chg=0 rc1=0 rc2=0
        local drvlist=""

        # trace only: get drivers for all subdevices
        if [ "$X_USB_DRIVER_TRACE" = "1" ]; then
            local dl
            drvlist=$(for dl in "$usbdev"/*:*/driver; do readlink "$dl" | \
                sed -r 's/.+\///'; done | sort -u | tr '\n' ' ')
            drvlist="(${drvlist% })"
        fi

        if wordinlist "$usbid" "$USB_ALLOWLIST"; then
            # device is in allowlist -- allowlist always wins
            control="auto"
            exc="_dev_allow"
        elif wordinlist "$usbid" "$USB_DENYLIST"; then
            # device is in denylist
            control="on"
            exc="_dev_deny"
        else
            local subdev

            # udev: wait for subdevices to populate
            [ "$caller" = "udev" ] && sleep 0.5

            # check for hid subdevices
            for subdev in "$usbdev"/*:*; do
                if [ "$(read_sysf $subdev/bInterfaceClass)" = "03" ]; then
                    control="on"
                    exc="_hid_deny"
                    break
                fi
            done

            if [ -z "$exc" ]; then
                # check for bluetooth devices
                if [ "$USB_EXCLUDE_BTUSB" = "1" ] \
                    && [ "$dclass" = "e0" ] \
                    && [ "$(read_sysf $usbdev/bDeviceSubClass)" = "01" ] \
                    && [ "$(read_sysf $usbdev/bDeviceProtocol)" = "01" ]; then
                    control="on"
                    exc="_btusb_deny"
                fi
            fi # bluetooth

            if [ -z "$exc" ]; then
                # check for audio devices
                if [ "$USB_EXCLUDE_AUDIO" = "1" ]; then
                    for subdev in "$usbdev"/*:*; do
                        if [ "$(read_sysf $subdev/bInterfaceClass)" = "01" ]; then
                            control="on"
                            exc="_audio_deny"
                            break
                        fi
                    done
                fi
            fi # audio

            if [ -z "$exc" ]; then
                # check for scanners:
                # libsane_matched envvar is set by libsane's udev rules
                # shellcheck disable=SC2154
                if [ "$libsane_matched" = "yes" ] || [ "$2" = "batch" ] \
                    && $UDEVADM info -q property $usbdev 2>/dev/null | grep -q 'libsane_matched=yes'; then
                    # do not touch this device
                    control="deny"
                    exc="_libsane"
                fi
            fi

            if [ -z "$exc" ]; then
                # check for phone devices
                if [ "$USB_EXCLUDE_PHONE" = "1" ]; then
                    if [ "$vendor" = "0fca" ]; then
                        # RIM
                        if [ "$dclass" = "ef" ]; then
                            # RIM / BlackBerry
                            control="on"
                            exc="_phone_deny"
                        elif [ "$dclass" = "00" ]; then
                           for subdev in "$usbdev"/*:*; do
                                if [ -d $subdev ]; then
                                    if [ "$(read_sysf $subdev/interface)" = "BlackBerry" ]; then
                                        # Blackberry
                                        control="on"
                                        exc="_phone_deny"
                                        break
                                    fi
                                fi
                            done
                        fi

                    elif [ "$vendor" = "045e" ] && [ "$dclass" = "ef" ]; then
                        # Windows Phone
                        control="on"
                        exc="_phone_deny"

                    elif [ "$vendor" = "05ac" ] && [ "$(read_sysf $usbdev/product)" = "iPhone" ]; then
                        # iPhone
                        control="on"
                        exc="_phone_deny"

                    elif [ "$dclass" = "00" ]; then
                        # class defined at interface level, iterate subdevices
                        for subdev in "$usbdev"/*:*; do
                            if [ -d $subdev ]; then
                                if [ "$(read_sysf $subdev/interface)" = "MTP" ]; then
                                    # MTP: mostly Android
                                    control="on"
                                    exc="_phone_deny"
                                    break
                                elif [ "$(read_sysf $subdev/bInterfaceClass)" = "ff" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceSubClass)" = "42" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceProtocol)" = "01" ]; then
                                    # ADB: Android
                                    control="on"
                                    exc="_phone_deny"
                                    break
                                elif [ "$(read_sysf $subdev/bInterfaceClass)" = "06" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceSubClass)" = "01" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceProtocol)" = "01" ]; then
                                    # PTP: iPhone, Lumia et al.
                                    # caveat: may also be a camera
                                    control="on"
                                    exc="_phone_deny"
                                    break
                                fi
                            fi
                        done

                    fi # dclass 00
                fi # exclude phone
            fi # phone

            if [ -z "$exc" ]; then
                # check for printers
                if [ "$USB_EXCLUDE_PRINTER" = "1" ]; then
                    if [ "$dclass" = "00" ]; then
                        # check for printer subdevices
                        for subdev in "$usbdev"/*:*; do
                            if [ "$(read_sysf $subdev/bInterfaceClass)" = "07" ]; then
                                control="on"
                                exc="_printer_deny"
                                break
                            fi
                        done
                    fi
                fi
            fi # printer

            if [ -z "$exc" ]; then
                # check for wwan devices
                if [ "$USB_EXCLUDE_WWAN" = "1" ]; then
                    if [ "$dclass" != "00" ]; then
                        # check for cdc subdevices
                        for subdev in "$usbdev"/*:*; do
                            if [ "$(read_sysf $subdev/bInterfaceClass)" = "0a" ]; then
                                control="on"
                                exc="_wwan_deny"
                                break
                            fi
                        done
                    fi

                    if [ -z "$exc" ]; then
                        # check for vendors
                        if wordinlist "$vendor" "$USB_WWAN_VENDORS"; then
                            control="on"
                            exc="_wwan_deny"
                        fi
                    fi
                fi # exclude wwan
            fi # wwan
        fi # !device denylist

        if [ "$(read_sysf $usbdev/power/control)" != "$control" ]; then
            # set control, write actual changes only
            case $control in
                auto|on)
                    write_sysf "$control" $usbdev/power/control; rc1=$?
                    chg=1
                    ;;

                deny) # do not touch denylisted device
                    ;;
            esac
        fi

        if [ "$X_TLP_USB_SET_AUTOSUSPEND_DELAY" = "1" ]; then
            # set autosuspend delay
            write_sysf $USB_TIMEOUT_MS $usbdev/power/autosuspend_delay_ms; rc2=$?
            echo_debug "usb" "usb_suspend_device.$caller.$control$exc: $busdev ID $usbid $usbdev [$drvlist]; control: rc=$rc1; delay: rc=$rc2"
        elif [ $chg -eq 1 ]; then
            # default: change control but not autosuspend_delay, i.e. keep kernel default setting
            echo_debug "usb" "usb_suspend_device.$caller.$control$exc: $busdev ID $usbid $usbdev [$drvlist]; control: rc=$rc1"
        else
            # we didn't change anything actually
            echo_debug "usb" "usb_suspend_device.$caller.$control$exc.no_change: $busdev ID $usbid $usbdev [$drvlist]"
        fi

    fi # autosuspendable

    return 0
}

set_usb_suspend () {
    # enable/disable usb autosuspend for all devices
    # $1: 0=silent/1=report result
    # $2: auto=enable/on=disable

    local usbdev

    if [ "$USB_AUTOSUSPEND" = "1" ]; then
        # autosuspend is configured --> iterate devices
        for usbdev in "$USBD"/*; do
            case "$usbdev" in
                *:*) ;; # colon in device name --> do nothing

                *) usb_suspend_device "$usbdev" "batch" $2 ;;
            esac
        done

        [ "$1" = "1" ] && echo "USB autosuspend settings applied."
        echo_debug "usb" "set_usb_suspend.done"

        # set "startup completion" flag for tlp-usb-udev
        set_run_flag $USB_DONE
    else
        [ "$1" = "1" ] && echo "Error: USB autosuspend is disabled. Set USB_AUTOSUSPEND=1 in ${CONF_USR}." 1>&2
        echo_debug "usb" "set_usb_suspend.not_configured"
    fi

    return 0
}

