
# This is the android file transfer interface.  Using adb
# it makes the andriod devices files available.

# package require -exact Expect 0.0.0
package require Expect
package provide VFSadb 1.21.03.02

namespace eval VFSadb {
  variable commands
  variable debug 0
  variable os $::tcl_platform(platform)
  variable abortflag
  
  array set commands [list\
			  open ::VFSadb::ADBopen\
			  reopen ::VFSadb::ADBopenLink\
			  live ::VFSadb::ADBlive\
			  cd ::VFSadb::ADBcd\
			  delete ::VFSadb::ADBdelete\
			  mkdir ::VFSadb::ADBmkdir\
			  rmdir ::VFSadb::ADBdelete\
			  rmdirEmpty 1\
			  isdir ::VFSadb::ADBisDir\
			  pwd ::VFSadb::ADBpwd\
			  chmod ::VFSadb::ADBchmod\
			  chown ::VFSadb::ADBchown\
			  list ::VFSadb::ADBlist\
			  get ::VFSadb::ADBget\
			  put ::VFSadb::ADBput\
			  copy ::VFSadb::ADBcopy\
			  RcopyOk 3\
			  rename ::VFSadb::ADBrename\
			  link ::VFSadb::ADBlink\
			  command ::VFSadb::ADBcommand\
			  close ::VFSadb::ADBclose\
			  search ::VFSadb::ADB_DoSearch\
			  debug ::VFSadb::ADB_debug]

  # Support routines *********************************
  proc eId {n} {return "(adb $n): "}
  # ******************* all with names "ADB_..."
  proc ADBlog_user {} {
    upvar VFStok VFStok
    VFSglobal tmpdir
    variable debug
    exp_log_user $debug
    if {$debug} {
      exp_log_file -a $tmpdir/adb.log
    }
  }
  proc dproc {args} {}

  proc ADBlog {mess} {
    upvar VFStok VFStok
    VFSglobal log
    eval [list {*}$log $mess]
  }

  # The open request.  We have host, user, password & port 
  # host and VFStok are usually the same and MUST not be ""
  # possible connection modes are:
  # option                       <user> value
  # USB      (-d adb option)     usb -nocase  i.e. -nocase *usb*
  # emulator (-e adb option)     emu -nocase  i.e. -nocase *emu*
  # serial # (-s adb option)     ser -nocase  i.e. -nocase *ser*
  #   serial implies usb              in this case <host> is taken 
  #                                   as the serial #
  # product  (-p adb option)     pro  -nocase  
  #                                   in this case <host> is taken 
  #                                   as the product id (-p argument)
  # connect <host>[:<port>]      con -nocase  i.e. -nocase *con*
  #                          we will get port 22 if the user has not
  #                          set it.  We morf this to 5555 otherwise
  #                          we use what was passed.
  # How we decode all this.. we use the <user> passed in to determine
  # how to connect.  If <password> contains 'su' root access is used 
  # on the device (adb shell;su etc.). 

  proc ADBopen {URL args} {
    VFSglobal user host port password tmpdir abortflag log op timeout spawn_id
    variable debug
    frputs args
    frputs  VFStok host user password port rq_timeout
    #  log_file -a ADB-EXP-LOG
    set debug 0
    set user {}
    set password {}
    set port 22
    set timeout {}
    set abortflag ::[namespace current]::abtflag
    set timefmt "%d%m%y "
    set tmpdir "/tmp"
    set log ::[namespace current]::dproc
    # The VFSadd (set by IsVFS) may contain a user name
    # and a port lets seperate them

    set poss {user password port timeout abortflag timefmt log debug tmpdir}
    foreach {opt value} [lindex $args 0] {
      set tar [lsearch -glob -all -inline $poss "$opt*"]
      if {$tar == {}} {continue}
      if {[llength $tar] != 1} {
	# puts "Here is tar >$tar<"
	error "[eId 1]ADBopen: option ($opt) must be one of: $poss"
      }
      set $tar $value
    }
    # over ride user and port if found in address
    regexp {(.*)@(.*)} $VFSadd ma user VFSadd
    regexp {(.*):(.*)} $VFSadd ma VFSadd port
    set host $VFSadd
   
    # set simp 0
    switch -nocase -glob $user {
      "*usb*" {set op "-d"}
      "*emu*" {set op "-e"}
      "*ser*" {set op "-s $host";set serialno $host}
      "*pro*" {set op "-p $host"}
      "*con*" {
	set addr $host:[expr {$port == 22 ? "5555" : $port}]
	set op "connect $addr";
	# set simp 1
      }
      default {return -code error "[eId 2]user name ($user) must be one of\
                                 'usb' 'emu' 'ser' 'pro' or 'con'"}
    }
    set mode [expr {[regexp -nocase {su} $password] ? "su" : ""}]
    # set timeout $rq_timeout
    set spawn_id -1
    set wd "/"
    set echo [expr {0 && $debug ? "" : "-noecho"}]
  # pelim done, lets see if we can talk...
    set pwd "/"
    return [ADBopenLink $VFStok]
  }

  proc ADBopenLink {VFStok} {
    VFSglobal echo op serialno timeout spawn_id

    catch exp_close
    ADBlog_user
    if {[string index $op 0] != "-"} {
      # This is a network connection...
      eval "spawn $echo adb dis$op"
      expect eof
      frputs op
      eval "spawn $echo adb $op"
      expect \
	  -re "(unable to connect .*)\r\n" {incr re 0} \
	  -re ".*connected to (\[^\r]*).*\n" {incr re } \
	  eof {set expect_out(1,string) "eof"; incr re 0}\
	  timeout {set expect_out(1,string) "timeout"; incr re 0}
      expect eof
      catch exp_close
      frputs re expect_out(1,string)
      if {![info exists re] || ! $re} {
	return -code error "[eId 3]$expect_out(1,string) Is device connected?"
      }
      set serialno $expect_out(1,string)
      ADBstartShell $VFStok
      return 0
    }
    # This is NOT a network connection
    # catch exp_close
    if {[string index $op 1] != "s"} {
      while {1} {
	catch exp_close
	frputs echo op
	eval "spawn $echo adb $op get-serialno"
	ADBlog_user
	set re 0
	expect \
	    -re "(\\* .*)\r\n" {incr re 4}\
	    -re "(error:\[^\r]*)\r\n" {incr re 3}\
	    -re "(unknown\[^\r]*)\r\n" {incr re 2} \
	    -re "(\[^\r]*)\r\n" {incr re} \
	    eof
	catch exp_close
	switch $re {
	  0 {return -code error "[eId 4]No response. Is device connected?" }
	  1 {
	    set serialno $expect_out(1,string)
	    ADBstartShell $VFStok
	    return {}}
	  2 {return -code error "[eId 5]$expect_out(1,string)\
                           May need to start server from root" }
	  3 {return -code error [eId 5]$expect_out(1,string) }
	  4 {
	    if {[incr count] > 2} {
	      return -code error "[eId 6]3 attempts to get serial\
                         number fail with: $expect_out(1,string)"
	    } else {
	      unset re
	    }
	  }
	}
      }
    }
    ADBstartShell $VFStok
  }

  proc ADBstartShell {VFStok } {
    VFSglobal prompt echo spawn_id serialno

    frputs serialno
    while {1} {
      frputs echo serialno
      eval "spawn $echo adb -s $serialno shell"
      expect \
	  -re "(\\* .*)\r\n" {incr re 3}\
	  -re "(error:\[^\r]*)\r\n" {incr re 2}\
	  -re {^(.+)$} {incr re} \
	  eof {incr re 0}\
	  timeout {set expect_out(1,string) "timeout"; incr re 3}
      
      frputs re expect_out(1,string) expect_out(2,string) expect_out(buffer)
      switch $re {
	0 {catch exp_close ;return -code error "[eId 7]Unexpected close of connection"}
	2 {catch exp_close ;return -code error [eId 8]$expect_out(1,string)}
	1 {
	  set pr [regsub {/} $expect_out(1,string) {.*}]
	  set pr [regsub {\$} $pr {\\$}]
	  set prompt $pr
	  break
	}
	3 {
	  if {[incr count] > 2} {
	    return -code error "[eId 9]3 attempts to start shell\
                         failed with: $expect_out(1,string)"
	  } else {
	    unset re
	    continue
	  }
	}
      }
    }
    if { $mode != "" } {
      exp_send "su\r"
      set re 0
      expect \
	  -re "su\r*\n" {exp_continue}\
	  -re "(error:\[^\r]*)\r\n" {incr re 2}\
	  -re "(^\[^ ]* )\$" {incr re} \
	  eof {incr re 0}
      frputs re expect_out(1,string)
      switch $re {
	0 {catch exp_close ;return -code error "[eId 10]Unexpected close of connection"}
	2 {catch exp_close ;return -code error [eId 11]$expect_out(1,string)}
	1 {
	  set pr [regsub {/} $expect_out(1,string) {.*}]
	  set pr [regsub {\$} $pr {\\$}]
	  set prompt $pr
	}
      }
    }
    catch  {exp_match_max 1000000}

    frputs prompt
  }

  proc ADBstopShell {VFStok} {
    VFSglobal spawn_id
    # upvar #0 VFSadb::adb($VFStok,handle) spawn_id
    exp_send "exit\r"
    catch exp_close
  }
  
  proc ADBsingle {VFStok what args} {
    VFSglobal prompt spawn_id timeout
    
    # This routine expects the adb connection to be in shell mode
    # This does a rather standard request and waits for a response.
    # While the title is 'single' one can pass more than one thing
    # in 'what'.  The eval will do the right thing by spaces 
    # (unless you want to save them, in which case you will want
    # to use FixFileName (likely level 3) like we do for 'file').

    # This routine returns any response other than a 'prompt' which gets
    # a null return.
    # eof and timeout throw errors.

    # set file [FixFileName $file -+ &]
    ADBlog_user
    set send [string trim [slList {*}$what {*}$args]]
    exp_send -- $send\r
    frputs what args timeout send
    # The first pattern takes care of the input line limit where back
    # spaces are inserted in the echo...
    expect \
	-re "^(${what}(\[^\n\]*\b+\[^\n\]*))+\r*\n?" {
	  frputs expect_out(buffer) expect_out(1,string)
	  exp_continue
	}\
	-re "^($send) *\r*\n?" {
	  frputs expect_out(buffer) expect_out(1,string) send prompt
	  exp_continue
	} \
	-re "^$prompt\$" {incr re 0} \
	-re "\r*\n*(.*)\r*\n($prompt){1}" {incr re} \
	-re "(.*)$prompt" {incr re} \
	-re "(.*)\[?\] ?" {incr re 3}\
	eof {set expect_out(1,string) "eof";incr re 2} \
	timeout {frputs "timeout ";set expect_out(1,string) "timeout";incr re 3
	}
    frputs re expect_out(buffer) expect_out(1,string)
    #just in case there is residue...
    expect -re ".*" {}
    switch $re {
      0 {return ""}
      3 {return -code error "[eId 13]Unexpected question: $expect_out(1,string)"}
      1 {
	set rtn [regsub -all {\r} $expect_out(1,string) {}]
	if {[string match *|* $rtn]} {
	  return -code error $rtn
	}
	return $rtn
      }
      default {return -code error  [regsub -all {\r} [eId 14]$expect_out(1,string) {}]}
    }
  }

  proc ADBcd {VFStok new_wd} {
    VFSglobal wd

    set re [ADBsingle $VFStok  cd $new_wd]
    if {$re == ""} {
      set wd  $new_wd
      return 1
    }
    return -code error [eId 15]$re
  }

  proc ADBisDir {VFStok dir} {
    set r [catch {ADBcd $VFStok $dir} really]
    if {$r == 0 || [string match -nocase *permission* $really]} {
      return 1
    }
    return 0
  }
  
  proc ADBrename {VFStok  old new} {
    # current name old change to new
    set re [ADBsingle $VFStok  "mv [FixFileName $old -+ &]" $new] 
    if {$re != "" } {
      return -code error [eId 16]$re
    }
    return ""
  }

  proc ADBdelete {VFStok filename} {
    # rm in toolbox and busybox both support -r
    # so we use this for files and dirs, always
    # with the -r

    return [ADBsingle $VFStok  rm -r $filename] 
  }

  proc ADBmkdir { VFStok dir } {
    # create dir mkdir,dir
    set re [ADBsingle $VFStok  mkdir $dir] 
    if {$re != "" } {
      return -code error [eId 17]$re
    }
    return 0
  }

  proc ADBlive { VFStok } {
    # we just require a simple transation to prove life..
    return [ADBsingle $VFStok "pwd" ""]
  }

  proc ADBpwd {VFStok} {
    # Want the real pwd, NOT the one adb thinks it is..
    return [ADBsingle $VFStok sh -c pwd]
  }

  proc ADBlist {URL all} {
    # We need VFStok for the call to single
    IsVFS $URL
    # use -l (long list) -f (no sort) -a depends on all
    # about 12.5k entries at 80 chars each (hay, just poke around on sourceforge)
    set op [expr {$all ? "-l -a" : "-l"}]
    set rtn [split [ADBsingle $VFStok ls {*}$op] "\n" ]
    if {[llength $rtn] < 3 && [string match -nocase *permission* $rtn]} {
      set errorInfo {}
      return -code error "[eId 18]Permission denied"
    }
    return $rtn
  }


  proc ADB_DoSearch { VFStok filename } {
    return -code error "[eId 19]Search not supported ... yet"
  }

  proc ADBreget { VFStok remoteFileName localFileName args} {
    return [ADBget $VFStok $remoteFileName $localFileName]
  }

  proc ADBget { VFStok remoteFileName localFileName args} {
    VFSglobal timeout echo serialno spawn_id

    # get  remoteFileName and put it localFileName
    # use -p to get full times etc.
    set remotename $remoteFileName
    set localfn $localFileName
    # set remotename [regsub -all " " $remoteFileName {\\ }]
    # set localfn [FixFileName $localFileName -+ &]
    frputs remoteFileName localfn
    eval [list spawn $echo adb -s $serialno\
	      pull $remotename \
	      $localfn]
    ADBlog_user

    expect \
	-re {(failed to .*)\r+\n} {incr re;frputs "expect_out(1,string)"} \
	-re {(remote object .*)\r+\n} {incr re 2;frputs "expect_out(1,string)"} \
	-re {\r*(.*bytes in *\[^\r\]*\r+\n)} \
	                  {incr re 3;frputs "expect_out(1,string)"} \
	-re {^([^\n]*)\r*\n} {ADBlog $expect_out(1,string);exp_continue}\
	eof {incr re 0}
    frputs re expect_out(1,string) expect_out(buffer)
    if {$re == 1 && $mode == "su"} {
    }
    if {$re} {return -code error "[eId 20]$expect_out(1,string)"}
    return $re
  }

  proc ADBput { VFStok localFileName remoteFileName args} {
    VFSglobal timeout spawn_id serialno

    frputs localFileName remoteFileName
    set localname [regsub -all " " $localFileName {\\ }]
    #
    eval "spawn $echo adb -s $serialno\
                      push $localname\
                      [FixFileName $remoteFileName -+ &]"
    ADBlog_user
    set re 0
    expect \
	-re "^(failed to copy .*)\r+\n" {incr re} \
	-re "^(Couldn't .*)\r+\n" {incr re} \
	-re "Fetching .*\r+\n" {exp_continue} \
	-re {^.*error: (.*)\r*\n} {incr re }\
	-re "\r.*$remoteFileName *(\[^ ].*:\[0-9]\[0-9])" {incr re 0}\
	-re {^([^\n]*)\r*\n} {ADBlog $expect_out(1,string);exp_continue}\
	-re "^\r+\n" {incr re 0}
    frputs re expect_out(1,string)
    if {$re} {return -code error "[eId 21]$expect_out(1,string)"}
    return $re
  }

  # cp -a is supported in both "toolbox" and "busybox"
  # fromFile may be a list of files...
  proc ADBcopy {VFStok fromFile toFile} {
    set re [ADBsingle $VFStok cp -a $fromFile $toFile]
    if {[string match *can't* $re]} {
      return -code error [eId 22]$re
    }
    
  }

  proc ADBlink { VFStok  exist new opt} {
    frputs exist new
    set lop [expr {$opt == "r" ? "-sr" : "-s"}]
    set re [ADBsingle $VFStok ln -s [FixFileName $exist -+ &] $new]
    if {$re == ""} {
      return ""
    }
    return -code error [eId 23]$re
  }

  proc ADBchmod {VFStok mode file} {
    ADBlog_user
    frputs  VFStok mode file
    return [ADBsingle $VFStok chmod $mode $file]
  }

  proc ADBchown {VFStok mode file} {
    ADBlog_user
    frputs  VFStok mode file
    return [ADBsingle $VFStok chown $mode $file]
  }

  proc ADBclose { VFStok} {
    VFSglobal spawn_id
    set spawn_id -1
  }


  proc ADBcommand {VFStok command} {
    ADBlog_user
    #  expect -re ".*"
    #  Log "sendingfrom pwd: pwd "
    set rtn [ADBsingle $VFStok {*}$command]
    if {[string match *|* $rtn]} {
      return -code error [eId 24]$rtn
    }
    return $rtn
  }

  proc ADB_debug {onoff} {
    variable debug
    set debug $onoff
  }
}
