#!/bin/sh
# tlp - handle added usb devices
#
# Copyright (c) 2017 Thomas Koch <linrunner at gmx.net>
# This software is licensed under the GPL v2 or later.

# Remark: the calling udev rule is triggered for "base" devices only,
#         not for the corresponding subdevices.

# --- Constants
readonly LOGGER=logger

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

readonly RUNDIR=/var/run/tlp
readonly USB_DONE=usb_done

readonly CONFFILE=/etc/default/tlp

# --- Subroutines
wordinlist () { # test if word in list
                # $1: word, $2: whitespace-separated list of words
    local word

    if [ -n "${1-}" ]; then
        for word in ${2-}; do
            [ "${word}" != "${1}" ] || return 0 # exact match
        done
    fi

    return 1 # no match
}

echo_debug () { # $1: tag; $2: msg; echo debug msg if tag matches
    if wordinlist "$1" "$TLP_DEBUG"; then
        $LOGGER -p debug -t "tlp[$$,$PPID]" "$2"
    fi
}

# --- MAIN

# read config
[ -f $CONFFILE ] || exit 0
. $CONFFILE

# exit if TLP or autosuspend disabled
[ "$TLP_ENABLE" = "1" ] && [ "$USB_AUTOSUSPEND" = "1" ] || exit 0

# USB autosuspend has two principal operation modes:
#
# Mode 1 (optional):
# - System startup is handled by tlp-functions:set_usb_suspend()
# - Startup completion is signaled by "flag file" $USB_DONE
# - Newly added devices are handled by this udev script
# - Mode 1 is enabled by the private config variable X_TLP_USB_MODE=1
#
# Mode 2 (default):
# - Everything - including system startup, but not shutdown - is handled by this udev script

# do exit if mode 1 and no startup completion flag
[ "$X_TLP_USB_MODE" = "1" ] && [ ! -f $RUNDIR/$USB_DONE ] && exit 0

# get args
usbdev=/sys$1

# handle device
if [ -f $usbdev/power/autosuspend ] || [ -f $usbdev/power/autosuspend_delay_ms ]; then
    # device is autosuspendable

    # apply autosuspend
    ctrlf="control"
    autof="autosuspend_delay_ms"
    vendor="$(cat $usbdev/idVendor)"
    usbid="$vendor:$(cat $usbdev/idProduct)"
    busdev="Bus $(cat $usbdev/busnum) Dev $(cat $usbdev/devnum)"
    dclass="$(cat $usbdev/bDeviceClass)"
    control="auto"
    exc=""
    chg=0
    drvlist=""

    # trace only: get drivers for all subdevices
    if [ "$X_USB_DRIVER_TRACE" = "1" ]; then
        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
        # wait for subdevices to populate
        sleep 0.5

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

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

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

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

            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 [ "$(cat $subdev/interface 2> /dev/null)" = "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" ] && [ "$(cat $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 [ "$(cat $subdev/interface 2> /dev/null)" = "MTP" ]; then
                                # MTP: mostly Android
                                control="on"
                                exc="_phone_black"
                                break
                            elif [ "$(cat $subdev/bInterfaceClass)" = "ff" ] \
                                && [ "$(cat $subdev/bInterfaceSubClass)" = "42" ] \
                                && [ "$(cat $subdev/bInterfaceProtocol)" = "01" ]; then
                                # ADB: Android
                                control="on"
                                exc="_phone_black"
                                break
                            elif [ "$(cat $subdev/bInterfaceClass)" = "06" ] \
                                && [ "$(cat $subdev/bInterfaceSubClass)" = "01" ] \
                                && [ "$(cat $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 wwan devices
            USB_BLACKLIST_WWAN=${USB_BLACKLIST_WWAN:-1} # default is exclude

            if [ "$USB_BLACKLIST_WWAN" = "1" ]; then
                if [ "$dclass" != "00" ]; then
                    # check for cdc subdevices
                    for subdev in $usbdev/*:*; do
                        if [ -d $subdev ] && [ "$(cat $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 [ -f $usbdev/power/control ]; then
        if [ "$(cat $usbdev/power/control)" != "$control" ]; then
            # Write actual changes only
            echo "$control" > $usbdev/power/control
            chg=1
        fi
    else
        # level is deprecated
        if [ "$(cat $usbdev/power/level)" != "$control" ]; then
            # Write actual changes only
            echo "$control" > $usbdev/power/level
            chg=1
        fi
        ctrlf="level"
    fi

    if [ "$X_TLP_USB_SET_AUTOSUSPEND_DELAY" = "1" ]; then
        # set autosuspend_delay
        if [ -f $usbdev/power/autosuspend_delay_ms ]; then
            echo $USB_TIMEOUT_MS > $usbdev/power/autosuspend_delay_ms
        else
            # autosuspend is deprecated
            echo $USB_TIMEOUT > $usbdev/power/autosuspend
            autof="autosuspend"
        fi
        echo_debug "usb" "udev_usb.$control$exc: $busdev ID $usbid $usbdev [$ctrlf $autof] $drvlist"
    elif [ $chg -eq 1 ]; then
        # default: change control but not autosuspend_delay, i.e. keep kernel default setting
        echo_debug "usb" "udev_usb.$control$exc: $busdev ID $usbid $usbdev [$ctrlf] $drvlist"
    else
        # we didn't change anything actually
        echo_debug "usb" "udev_usb.$control$exc: $busdev ID $usbid $usbdev $drvlist"
    fi
fi

exit 0
