#!/bin/sh
# tlp-func-usb - USB Functions
#
# Copyright (c) 2019 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=2
readonly USB_TIMEOUT_MS=2000
readonly USB_WWAN_VENDORS="0bdb 05c6 1199 2cb7"
readonly USB_DONE=usb_done

readonly DEFAULT_USB_AUTOSUSPEND=0
readonly DEFAULT_USB_BLACKLIST_BTUSB=0
readonly DEFAULT_USB_BLACKLIST_PHONE=0
readonly DEFAULT_USB_BLACKLIST_PRINTER=1
readonly DEFAULT_USB_BLACKLIST_WWAN=0

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

# --- USB Autosuspend

usb_suspend_device () { # enable/disable usb autosuspend for a single device
    # except input and blacklisted
    # $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_WHITELIST"; then
            # device is in whitelist -- whitelist always wins
            control="auto"
            exc="_dev_white"
        elif wordinlist "$usbid" "$USB_BLACKLIST"; then
            # device is in blacklist
            control="on"
            exc="_dev_black"
        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_black"
                    break
                fi
            done

            if [ -z "$exc" ]; then
                # check for bluetooth devices
                : ${USB_BLACKLIST_BTUSB:=${DEFAULT_USB_BLACKLIST_BTUSB}}

                if [ "$USB_BLACKLIST_BTUSB" = "1" ] \
                    && [ "$dclass" = "e0" ] \
                    && [ "$(read_sysf $usbdev/bDeviceSubClass)" = "01" ] \
                    && [ "$(read_sysf $usbdev/bDeviceProtocol)" = "01" ]; then
                    control="on"
                    exc="_btusb_black"
                fi
            fi # bluetooth

            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="black"
                    exc="_libsane"
                fi
            fi

            if [ -z "$exc" ]; then
                # check for phone devices
                : ${USB_BLACKLIST_PHONE:=${DEFAULT_USB_BLACKLIST_PHONE}}

                if [ "$USB_BLACKLIST_PHONE" = "1" ]; then
                    if [ "$vendor" = "0fca" ]; then
                        # RIM
                        if [ "$dclass" = "ef" ]; then
                            # RIM / BlackBerry
                            control="on"
                            exc="_phone_black"
                        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_black"
                                        break
                                    fi
                                fi
                            done
                        fi

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

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

                    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_black"
                                    break
                                elif [ "$(read_sysf $subdev/bInterfaceClass)" = "ff" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceSubClass)" = "42" ] \
                                    && [ "$(read_sysf $subdev/bInterfaceProtocol)" = "01" ]; then
                                    # ADB: Android
                                    control="on"
                                    exc="_phone_black"
                                    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_black"
                                    break
                                fi
                            fi
                        done

                    fi # dclass 00
                fi # blacklist phone
            fi # phone

            if [ -z "$exc" ]; then
                # check for printers
                : ${USB_BLACKLIST_PRINTER:=${DEFAULT_USB_BLACKLIST_PRINTER}}

                if [ "$USB_BLACKLIST_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_black"
                                break
                            fi
                        done
                    fi
                fi
            fi # printer

            if [ -z "$exc" ]; then
                # check for wwan devices
                : ${USB_BLACKLIST_WWAN:=${DEFAULT_USB_BLACKLIST_WWAN}}

                if [ "$USB_BLACKLIST_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_black"
                                break
                            fi
                        done
                    fi

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

        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
                    ;;

                black) # do not touch blacklisted 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

    check_sysfs "set_usb_suspend" "$USBD"

    : ${USB_AUTOSUSPEND:=${DEFAULT_USB_AUTOSUSPEND}}

    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"
    else
        [ "$1" = "1" ] && echo "Error: USB autosuspend is disabled. Set USB_AUTOSUSPEND=1 in $CONFFILE." 1>&2
        echo_debug "usb" "set_usb_suspend.not_configured"

    fi

    # set "startup completion" flag for tlp-usb-udev
    set_run_flag $USB_DONE

    return 0
}

