{*******************************************************}
{                                                       }
{       Tocsg.USB                                       }
{                                                       }
{       Copyright (C) 2022 kku                          }
{                                                       }
{*******************************************************}

unit Tocsg.USB;

interface

uses
  Tocsg.Obj, System.Classes, System.SysUtils, Winapi.Windows,
  System.Generics.Collections, Winapi.Messages;

const
  GUID_DEVINTERFACE_USB_DEVICE: TGUID = '{A5DCBF10-6530-11D2-901F-00C04FB951ED}';

  DBT_DEVICEARRIVAL            = $8000;      // system detected a new device
  DBT_DEVICEREMOVECOMPLETE     = $8004;      // device is gone
  DBT_DEVICEQUERYREMOVE        = $8001;
  DBT_DEVNODES_CHANGED         = $0007;

  DBTF_MEDIA                   = $0001;
  DBT_DEVTYP_VOLUME            = $0002;
  DBT_DEVTYP_PORT              = $0003;
  DBT_DEVTYP_NET               = $0004;
  DBT_DEVTYP_DEVICEINTERFACE   = $0005;      // device interface class
  DBT_DEVTYP_HANDLE            = $0006;

type
  PDevBroadcastHdr = ^DEV_BROADCAST_HDR;
  DEV_BROADCAST_HDR = {$IFNDEF WIN64} packed {$ENDIF} record
    dwSize       : DWORD;
    dwDevicetype : DWORD;
    dwReserved   : DWORD;
  end;

  PDevBroadcastDeviceInterface = ^DEV_BROADCAST_DEVICEINTERFACE;
  DEV_BROADCAST_DEVICEINTERFACE = record
    dwSize       : DWORD;
    dwDevicetype : DWORD;
    dwReserved   : DWORD;
    ClassGUID    : TGUID;
    nName        : Short;
  end;

  PDevBroadcastVolume = ^TDevBroadcastVolume;
  TDevBroadcastVolume = {$IFNDEF WIN64} packed {$ENDIF} record
    dwSize: DWORD;
    dwDevicetype: DWORD;
    dwReserved: DWORD;
    dwUnitmask: DWORD;
    wFlags: Word;
  end;

  PDevBroadcastHandle = ^TDevBroadcastHandle;
  DEV_BROADCAST_HANDLE = record
    dbch_size: DWORD;
    dbch_devicetype: DWORD;
    dbch_reserved: DWORD;
    dbch_handle: THandle;            { file handle used in call to RegisterDeviceNotification }
    dbch_hdevnotify: HDEVNOTIFY;          { HDEVNOTIFY returned from RegisterDeviceNotification }

    { The following 3 fields are only valid if wParam is DBT_CUSTOMEVENT. }

    dbch_eventguid: TGUID;
    dbch_nameoffset: LongInt;           { offset (bytes) of variable-length string buffer (-1 if none)}
    dbch_data: array[0..0] of BYTE;   { variable-sized buffer, potentially containing binary and/or text data }
  end;
  TDevBroadcastHandle = DEV_BROADCAST_HANDLE;

  TUSBChangeEvent = procedure(Sender: TObject; pInfo: PDevBroadcastVolume) of object;
  TDevChangeEvent = procedure(Sender: TObject; pInfo: PDevBroadcastDeviceInterface) of object;
  TUSBChangeQueryEvent = procedure(Sender: TObject; sDrive: String; var bAccept: Boolean) of object;

  TTgUSBEventNotify = class(TTgObject)
  private
    hWindowHandle_    : HWND;
    evUSBArrival_,
    evUSBRemove_      : TUSBChangeEvent;
    evDevArrival_,
    evDevRemove_      : TDevChangeEvent;
    evUSBQueryRemove_ : TUSBChangeQueryEvent;
    DcQueryRemoveNotify_: TDictionary<HDEVNOTIFY,String>;

  // 특정 환경에서 USB 연결/해제 시 DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE 값을
  // 제대로 받아오지 못하는 경우가 있다. 이 경우 기존 드라이브를 비교해서 연결/해제를 판단함.
    dwLogicalDrvs_ : DWORD;
    bDeviceChanging_: Boolean;

    procedure ProcessWindowMessage(var msg: TMessage);
    function RegisterDeviceChange(sPath: String): Boolean; overload;
    function GetQueryRemovePath(hDev: HDEVNOTIFY): String;
    procedure SetEventQueryRemove(evUSBChangeQueryEvent: TUSBChangeQueryEvent);
  protected
    procedure process_WM_DEVICECHANGE(var msg: TMessage);
  public
    constructor Create;
    destructor Destroy; override;
    function RegisterDeviceChange: Boolean; overload;// not use
  published
    property OnUSBArrival: TUSBChangeEvent write evUSBArrival_;
    property OnUSBQueryRemove: TUSBChangeQueryEvent write SetEventQueryRemove;
    property OnUSBRemove: TUSBChangeEvent write evUSBRemove_;
    property OnDevArrival: TDevChangeEvent write evDevArrival_;
    property OnDevRemove: TDevChangeEvent write evDevRemove_;
  end;

const
  REG_ENUM_USB                    = 'SYSTEM\CurrentControlSet\Enum\USB\';
  REG_ENUM_USBSTOR                = 'SYSTEM\CurrentControlSet\Enum\USBSTOR\';
  REG_ENUM_USE_DISK_NUM           = 'SYSTEM\CurrentControlSet\Services\disk\Enum\'; // 현재 인식된 물리디스크 넘버를 확인할수 있다.
  REG_MOUNTED_DEVICES             = 'SYSTEM\MountedDevices\';
  REG_USB_LASTWRITE_CONTROL1      = 'SYSTEM\CurrentControlSet\Control\DeviceClasses\{53f56307-b6bf-11d0-94f2-00a0c91efb8b}\##?#USBSTOR#%s#%s#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}\Control';
  REG_USB_LASTWRITE_CONTROL2      = 'SYSTEM\CurrentControlSet\Control\DeviceClasses\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}\##?#STORAGE#RemovableMedia#%s&RM#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}\Control';
  REG_USB_LASTWRITE_CONTROL_VISTA = 'SYSTEM\CurrentControlSet\Control\DeviceClasses\{53f56307-b6bf-11d0-94f2-00a0c91efb8b}\##?#USBSTOR#%s#%s#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}\Control';

type
  PUSBRec = ^TUSBRec;
  TUSBRec = record
    sDeviceName,
    sDescription,
    sDriveLetter,
    sFriendlyName,
    sSerial,
    sParentIdPrefix,
    sVID,
    sPID,
    sUsbstor,
    sSerial2 : AnsiString;
    dtCreate,
    dtLastWrite: TDateTime;
    nDiskNum : Integer; // 연결되어 있다면 값이, 없다면 -1
  end;

  TTgUSBStorInfo = class(TObject)
  private
    bIsVista_: Boolean;
  protected
    USBStorList_: TList<PUSBRec>;

    procedure OnUSBStorNotify(Sender: TObject; const Item: PUSBRec;
                              Action: TCollectionNotification);

    function GetCount: Integer;
    function GetUSBInfoStep_1: Boolean;
    function GetUSBInfoStep_2: Boolean;
    function GetUSBInfoStep_3: Boolean;
    function GetUSBInfoStep_4: Boolean;
    function GetUSBInfoStep_5: Boolean;
  public
    Constructor Create;
    Destructor Destroy; override;

    function GetUSBInfoByLetter(cDrive: AnsiChar): PUSBRec;

    function GetInfo(nIndex: Integer): PUSBRec;
    procedure PutInfo(nIndex: Integer; const pData: PUSBRec);

    function UpdateUSBStorInfo: Boolean;

//    function RemoveUSBInfo(pData: PUSBRec) : Boolean;

    property USBRecs[nIndex: Integer]: PUSBRec read GetInfo; default;
    property Count: Integer read GetCount;
  end;

implementation

uses
  Tocsg.Disk, System.Win.Registry, Tocsg.Safe,
  Tocsg.DateTime, Tocsg.Exception, EM.WinOSVersion,
  Tocsg.Registry;

{ TTgUSBEventNotify }

// 이거 스레드에서 생성하면 오동작 할 가능성이 농후함.
// 내부적으로 다열로그를 만들어서 메세지를 받기 때문..
Constructor TTgUSBEventNotify.Create;
begin
  {$IFDEF TRACE1} _Trace('Create()'); {$ENDIF}
  inherited Create;

  DcQueryRemoveNotify_ := TDictionary<HDEVNOTIFY,String>.Create;

  dwLogicalDrvs_ := GetLogicalDrives;
  bDeviceChanging_ := false;

  hWindowHandle_ := AllocateHWnd(ProcessWindowMessage);
end;

Destructor TTgUSBEventNotify.Destroy;
begin
  DeallocateHWnd(hWindowHandle_);
  FreeAndNil(DcQueryRemoveNotify_);

  inherited;
  {$IFDEF TRACE1} _Trace('Destroy()'); {$ENDIF}
end;

procedure TTgUSBEventNotify.SetEventQueryRemove(evUSBChangeQueryEvent: TUSBChangeQueryEvent);

  procedure register_notify;
  var
    sDrive: String;
    nDrive: Integer;
  begin
    for nDrive := 2 to 31 do
    begin
      sDrive := Format('%s:\', [Char(Integer('A')+nDrive)]);

      case GetDriveType(PChar(sDrive)) of
        DRIVE_FIXED,
        DRIVE_REMOVABLE :
          if GetDriveSize(sDrive) <> 0 then
            RegisterDeviceChange(sDrive);

      end;
    end;
  end;

begin
  DcQueryRemoveNotify_.Clear;
  evUSBQueryRemove_ := evUSBChangeQueryEvent;
  if Assigned(evUSBQueryRemove_) then
    register_notify;
end;

function TTgUSBEventNotify.GetQueryRemovePath(hDev: HDEVNOTIFY): String;
begin
  if DcQueryRemoveNotify_.ContainsKey(hDev) then
    Result := DcQueryRemoveNotify_[hDev]
  else
    Result := '';
end;

procedure TTgUSBEventNotify.ProcessWindowMessage(var msg: TMessage);
begin
  case Msg.Msg of
    WM_DEVICECHANGE : process_WM_DEVICECHANGE(msg);
  end;
  Msg.Result := DefWindowProc(hWindowHandle_, msg.Msg, msg.wParam, msg.lParam);
end;

procedure TTgUSBEventNotify.process_WM_DEVICECHANGE(var msg : TMessage);
var
//  nTime: Integer;
  dwAddDrv,
  dwDelDrv,
  dwNewDrvs : DWORD;
  pInfo: PDevBroadcastVolume;
  bAccept: Boolean;
  sDrive: String;

  procedure compare_drives;
  begin
    dwAddDrv := dwLogicalDrvs_ xor dwNewDrvs;
    dwDelDrv := dwAddDrv;

    dwAddDrv := dwNewDrvs and dwAddDrv;
    dwDelDrv := dwLogicalDrvs_ and dwDelDrv;
  end;

  procedure ProcessChangeDevice(dwDrvs: DWORD; bAdd: Boolean);
  var
    i: Integer;
    m: DWORD;
  begin
    if dwDrvs = 0 then
      exit;

    for i := 0 to 31 do
    begin
      m := dwDrvs and (1 shl i);
      if m <> 0 then
      begin
      // 일단 이것만 쓰니깐 이것만 채워준다
        pInfo.dwUnitmask := m;
        if bAdd and Assigned(evUSBArrival_) then
        begin
          evUSBArrival_(self, pInfo);

        // 추가 22_0504 13:49:21 kku
          if Assigned(evUSBQueryRemove_) then
          begin
            var sDrive: String := GetDriveFromMask(pInfo.dwUnitmask);
            if GetDriveExtent(sDrive).liExtentLength.QuadPart <> 0 then
            begin
              if GetDriveSize(sDrive) <> 0 then
                case Integer(GetDriveType(PChar(sDrive))) of
                  DRIVE_REMOVABLE,
                  DRIVE_FIXED : RegisterDeviceChange(sDrive);
                end;
            end;
          end;
        end else
        if not bAdd and Assigned(evUSBRemove_) then
          evUSBRemove_(self, pInfo);
      end;
    end;
  end;

begin
// DBT_DEVICEQUERYREMOVE
// DBT_DEVICEQUERYREMOVEFAILED
// DBT_DEVICEREMOVECOMPLETE

  // flags & DBTF_MEDIA = 1  ->  씨디룸
  // flags & DBTF_MEDIA = 2  ->  네트워크 드라이브

// 반응속도가 참 느리네.. 비스타에서!
  case msg.WParam of
  {$IF true}
    DBT_DEVICEARRIVAL :
    begin
      case PDevBroadcastHdr(msg.LParam).dwDevicetype of
        DBT_DEVTYP_VOLUME :
        begin
          if Assigned(evUSBArrival_) then
          begin
            evUSBArrival_(self, PDevBroadcastVolume(msg.LParam));

          // 추가 22_0504 13:49:21 kku
            if Assigned(evUSBQueryRemove_) then
            begin
              sDrive := GetDriveFromMask(PDevBroadcastVolume(msg.LParam).dwUnitmask);
              if GetDriveExtent(sDrive).liExtentLength.QuadPart <> 0 then
              begin
                if GetDriveSize(sDrive) <> 0 then
                  case Integer(GetDriveType(PChar(sDrive))) of
                    DRIVE_REMOVABLE,
                    DRIVE_FIXED : RegisterDeviceChange(sDrive);
                  end;
              end;
            end;
          end;

          sDrive := GetDriveFromMask(PDevBroadcastVolume(msg.LParam).dwUnitmask);
          if Assigned(evUSBQueryRemove_) then
            RegisterDeviceChange(sDrive);
  //        begin
  //          if register_deviceChange(sDrive) then
  //            OutputDebugString('register_deviceChange Succss~~~~~~~~~~~~~~~~~')
  //          else
  //            OutputDebugString('register_deviceChange Fail!!!!!!!!!!!!!!!!!!!!!');
  //        end;
        end;
        DBT_DEVTYP_DEVICEINTERFACE :
        begin
          if Assigned(evDevArrival_) then
          begin
            evDevArrival_(Self, PDevBroadcastDeviceInterface(msg.LParam));
            _Trace('DevArrival = %s', [GUIDToString(PDevBroadcastDeviceInterface(msg.LParam).ClassGUID)]);
          end;
        end;
      end;
    end;
    DBT_DEVICEQUERYREMOVE :
    begin
      if PDevBroadcastHdr(msg.LParam).dwDevicetype = DBT_DEVTYP_HANDLE then
        if Assigned(evUSBQueryRemove_) then
        begin
          sDrive := GetQueryRemovePath(PDevBroadcastHandle(msg.LParam).dbch_hdevnotify);
          if sDrive <> '' then
          begin
            bAccept := true;
            evUSBQueryRemove_(self, sDrive, bAccept);
            if not bAccept then // 거부임? 그럼 거부
              msg.Result := BROADCAST_QUERY_DENY;
          end;
        end;
    end;
    DBT_DEVICEREMOVECOMPLETE :
    begin
      case PDevBroadcastHdr(msg.LParam).dwDevicetype of
        DBT_DEVTYP_VOLUME :
        begin
          if Assigned(evUSBRemove_) then
            evUSBRemove_(self, PDevBroadcastVolume(msg.LParam));
        end;
        DBT_DEVTYP_HANDLE :
        begin
          try
            DcQueryRemoveNotify_.Remove(PDevBroadcastHandle(msg.LParam).dbch_hdevnotify);
          except
          // 건덕지 없을듯
          end;
        end;
        DBT_DEVTYP_DEVICEINTERFACE :
        begin
          if Assigned(evDevRemove_) then
          begin
            evDevRemove_(Self, PDevBroadcastDeviceInterface(msg.LParam));
            _Trace('DevRemove = %s', [GUIDToString(PDevBroadcastDeviceInterface(msg.LParam).ClassGUID)]);
          end;
        end;
      end;
    end;
  {$ELSE}
  // 이건 특정한 상황(?)에서 메세지가 이거 빼곤 다른게 안받아져서 구현한 거임..
  // 급해서 이렇게 만들어서 썼는데 이거보단 위에껄 추천함
    DBT_DEVNODES_CHANGED :
    begin
      if bDeviceChanging_ then
        exit;

      bDeviceChanging_ := true;
      dwNewDrvs := GetLogicalDrives;

      nTime := 0;
      while nTime < 5000 do
      begin
        if dwLogicalDrvs_ <> dwNewDrvs then
        begin
          New(pInfo);
          ZeroMemory(pInfo, SizeOf(TDevBroadcastVolume));
          try
            compare_drives;
            dwLogicalDrvs_ := dwNewDrvs;

            ProcessChangeDevice(dwAddDrv, true);
            ProcessChangeDevice(dwDelDrv, false);
          finally
            Dispose(pInfo);
            bDeviceChanging_ := false;
          end;
          break;
        end;
        dwNewDrvs := GetLogicalDrives;
        WaitForTimer(10);
        Inc(nTime);
      end;
    end;
  {$IFEND}
  end;
end;

// not use
function TTgUSBEventNotify.RegisterDeviceChange: Boolean;
var
  dbi   : DEV_BROADCAST_DEVICEINTERFACE;
  nSize : Integer;
  hDev  : HDEVNOTIFY;
begin
  Result := False;
  nSize := SizeOf(DEV_BROADCAST_DEVICEINTERFACE);
  ZeroMemory(@dbi, nSize);
  dbi.dwSize := nSize;
  dbi.dwDevicetype := DBT_DEVTYP_DEVICEINTERFACE;
  dbi.ClassGUID := GUID_DEVINTERFACE_USB_DEVICE;

  hDev := RegisterDeviceNotification(hWindowHandle_,
                                     @dbi,
                                     DEVICE_NOTIFY_WINDOW_HANDLE);

  if hDev <> nil then
    Result := True;
end;

function TTgUSBEventNotify.RegisterDeviceChange(sPath: String): Boolean;
var
  hDev      : HDEVNOTIFY;
  hPath     : THandle;
  DevHandle : TDevBroadcastHandle;
begin
  Result := false;

  try
  // 윈도우 xp에서 빈디스크 에러 메시지 걸러내기
    if GetDriveExtent(sPath).liExtentLength.QuadPart = 0 then
      exit;

    if not DirectoryExists(sPath) then
      exit;

    hPath := CreateFile(PChar(sPath),
                        GENERIC_READ,
                        FILE_SHARE_READ or FILE_SHARE_WRITE,
                        nil,
                        OPEN_EXISTING,
                        FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL,
                        0);

    if hPath = INVALID_HANDLE_VALUE then
      exit;

    try
      ZeroMemory(@DevHandle, SizeOf(TDevBroadcastHandle));
      DevHandle.dbch_size       := SizeOf(TDevBroadcastHandle);
      DevHandle.dbch_devicetype := DBT_DEVTYP_HANDLE;
      DevHandle.dbch_handle     := hPath;

      hDev := RegisterDeviceNotification(hWindowHandle_,
                                         @DevHandle,
                                         DEVICE_NOTIFY_WINDOW_HANDLE);

      if hDev <> nil then
      begin
        DcQueryRemoveNotify_.Add(hDev, sPath);
        Result := true;
      end;
    finally
      CloseHandle(hPath);
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E);
  end;
end;

procedure Get_VID_PID(sInfo: AnsiString; var sVid: AnsiString; var sPid: AnsiString);
var
  nPos1, nPos2, nLen: Integer;
begin
  sInfo := LowerCase(sInfo);
  nPos1 := Pos('vid_', sInfo);
  nPos2 := Pos('pid_', sInfo);
  if (nPos1 = 0) or (nPos2 = 0) then exit;

  nLen := Length(sInfo);
  sVid := Copy(sInfo, nPos1+4, nLen-nPos2-3);
  sPid := Copy(sInfo, nPos2+4, nLen-nPos2-3);
end;

{ TTgUSBStorInfo }

Constructor TTgUSBStorInfo.Create;
var
  ver: TWinVerInfo;
begin
  Inherited Create;

  bIsVista_ := false;

  ver := GetWinVersion;
// 비스타나 체크한다
  if ver.Version.Major = 6 then
    bIsVista_ := true;

  USBStorList_ := TList<PUSBRec>.Create;
  USBStorList_.OnNotify := OnUSBStorNotify;
end;

Destructor TTgUSBStorInfo.Destroy;
begin
  FreeAndNil(USBStorList_);
  Inherited;
end;

procedure TTgUSBStorInfo.OnUSBStorNotify(Sender: TObject; const Item: PUSBRec;
                                          Action: TCollectionNotification);
begin
  case Action of
    cnAdded: ;
    cnRemoved: Dispose(Item);
    cnExtracted: ;
  end;
end;

function TTgUSBStorInfo.GetUSBInfoByLetter(cDrive: AnsiChar): PUSBRec;
var
  pData: PUSBRec;
  i: Integer;
begin
  Result := nil;

  for i := 0 to USBStorList_.Count - 1 do
  begin
    pData := USBStorList_[i];
    if (pData.sDriveLetter <> '') and
       (pData.sDriveLetter[1] = cDrive) then
    begin
      Result := pData;
      exit;
    end;
  end;
end;

function TTgUSBStorInfo.GetCount: Integer;
begin
  Result := USBStorList_.Count;
end;

function TTgUSBStorInfo.GetInfo(nIndex: Integer): PUSBRec;
begin
  if (nIndex >= 0) and (nIndex < Count) then
    Result := USBStorList_[nIndex]
  else
    Result := nil;
end;

procedure TTgUSBStorInfo.PutInfo(nIndex: Integer; const pData: PUSBRec);
begin
  if (nIndex >= 0) and (nIndex < Count) then
    USBStorList_[nIndex] := pData;
end;

// USB 목록에서 Service가 USBSTOR인것을 추출하여
// 장치이름, 설명, 생성일자, 시리얼 넘버, vid, pid를 얻어온다.
function TTgUSBStorInfo.GetUSBInfoStep_1: Boolean;
var
  pData             : PUSBRec;
  reg, regSub       : TRegistry;
  lstKey,
  lstSubKey,
  lstSubSubKey      : TStringList;
  i, j, n           : Integer;
  sKey              : String;
  dtCreate,
  dtCompCreate      : TDateTime;
  regInfo           : TRegKeyInfo;
begin
  Result := false;

  Guard(reg, TRegistry.Create);
  reg.RootKey := HKEY_LOCAL_MACHINE;
  if not reg.OpenKeyReadOnly(REG_ENUM_USB) then
  begin
//    ASSERT(false);
    exit;
  end;

  Guard(regSub, TRegistry.Create);
  regSub.RootKey := HKEY_LOCAL_MACHINE;

  Guard(lstKey, TStringList.Create);
  Guard(lstSubKey, TStringList.Create);
  Guard(lstSubSubKey, TStringList.Create);

  reg.GetKeyNames(lstKey);

  for i := 0 to lstKey.Count - 1 do
  begin
    sKey := REG_ENUM_USB + lstKey[i] + '\';

    lstSubKey.Clear;
    reg.CloseKey;
    if not reg.OpenKeyReadOnly(sKey) then
    begin
//      ASSERT(false);
      exit;
    end;

    reg.GetKeyNames(lstSubKey);

    for j := 0 to lstSubKey.Count - 1 do
    begin
      reg.CloseKey;
      if not reg.OpenKeyReadOnly(sKey + lstSubKey[j]) then
      begin
//        ASSERT(false);
        exit;
      end;

    // 이 시간이.. 하위 키값때문인지 바뀔수도 있다..
    // 하위 키값이 더 오래된거면 그걸로 맞춘다. 
      if reg.GetKeyInfo(regInfo) then
      begin
        dtCreate := ConvFileTimeToDateTime_Local(regInfo.FileTime);
        reg.GetKeyNames(lstSubSubKey);
        for n := 0 to lstSubSubKey.Count - 1 do
          if regSub.OpenKeyReadOnly(sKey + lstSubKey[j] + '\' + lstSubSubKey[n]) then
          begin
            if regSub.GetKeyInfo(regInfo) then
            begin
              dtCompCreate := ConvFileTimeToDateTime_Local(regInfo.FileTime);
              if dtCompCreate < dtCreate then
                dtCreate := dtCompCreate;
            end;
            regSub.CloseKey;
          end;
      end else
        dtCreate := 0;

      if LowerCase(reg.ReadString('Service')) = 'usbstor' then
      begin
        New(pData);
        ZeroMemory(pData, SizeOf(TUSBRec));
        pData.sDeviceName := reg.ReadString('LocationInformation');
        pData.sDescription := reg.ReadString('DeviceDesc');

      // 외장 USB하드는 ParentIdPrefix가 여기에 위치하는거 같다..
      // 이건.. serial2로 넣도록하자
      // USBSTOR에 또 ParentIdPrefix가 있는데 이건 볼륨명 찾을때 필요하다.
      // 둘이 무슨차이가 있는거지..?
        if reg.ValueExists('ParentIdPrefix') then
        begin
          pData.sSerial2 := reg.ReadString('ParentIdPrefix');
        end else begin
          pData.sSerial2 := lstSubKey[j];
          if Length(pData.sSerial2) > 30 then
            SetLength(pData.sSerial2, 30);
        end;

//        n := Pos('&', lstSubKey[j]);
//        if n = 0 then pData.sSerial := lstSubKey[j];

        pData.sSerial := lstSubKey[j];
        if Length(pData.sSerial) > 30 then
          SetLength(pData.sSerial, 30);

//        pData.sSerial2 := lstSubKey[j];
        Get_VID_PID(Ansistring(lstKey[i]), pData.sVid, pData.sPid);
        pData.dtCreate := dtCreate;
        pData.nDiskNum := -1;

        USBStorList_.Add(pData);
      end;
    end;
  end;

  Result := true;
end;

// 드라이브 볼륨명을 얻기위해 ParentIdPrefix 값을 구하고
// USB의 FriendlyName을 구한다.
function TTgUSBStorInfo.GetUSBInfoStep_2: Boolean;
var
  pData             : PUSBRec;
  reg               : TRegistry;
  lstKey, lstSubKey : TStringList;
  sKey              : String;
  i, j, c           : Integer;
begin
  Result := false;

  Guard(reg, TRegistry.Create);
  reg.RootKey := HKEY_LOCAL_MACHINE;
  if not reg.KeyExists(REG_ENUM_USBSTOR) then
  begin
    Result := True;
    Exit;
  end;
  if not reg.OpenKeyReadOnly(REG_ENUM_USBSTOR) then
  begin
//    ASSERT(false);
    exit;
  end;

  Guard(lstKey, TStringList.Create);
  Guard(lstSubKey, TStringList.Create);

  reg.GetKeyNames(lstKey);

  for i := 0 to lstKey.Count - 1 do
  begin
    sKey := REG_ENUM_USBSTOR + lstKey[i] + '\';

    lstSubKey.Clear;
    reg.CloseKey;
    if not reg.OpenKeyReadOnly(sKey) then
    begin
//      ASSERT(false);
      exit;
    end;

    reg.GetKeyNames(lstSubKey);
    for j := 0 to lstSubKey.Count - 1 do
    begin
      reg.CloseKey;
      if not reg.OpenKeyReadOnly(sKey + lstSubKey[j]) then
      begin
//        ASSERT(false);
        exit;
      end;

      for c := 0 to USBStorList_.Count - 1 do
      begin
        pData := USBStorList_[c];

        if Pos(pData.sSerial, lstSubKey[j]) > 0 then
        begin
          if reg.ValueExists('ParentIdPrefix') then
            pData.sParentIdPrefix := reg.ReadString('ParentIdPrefix');

          if pData.sFriendlyName = '' then
            pData.sFriendlyName := reg.ReadString('FriendlyName');

        // vista의 경우 추가 정보획득..
//          if bIsVista_ then
          begin
            pData.sUsbstor := lstKey[i];
            pData.sSerial2 := lstSubKey[j];
          end;
        end else
        if lstSubKey[j] = pData.sSerial2 then
        begin
        // 외장 USB하드의 경우를 위해 이렇게 처리
        // 마지막 사용일자를 구하는 위치가 다른데.. 그 위치를 구하기 위한 준비
          if reg.ValueExists('ParentIdPrefix') then
            pData.sParentIdPrefix := reg.ReadString('ParentIdPrefix');
          pData.sUsbstor := lstKey[i];
//          pData.sSerial2 := lstSubKey[j];
        end;
      end;
    end;
  end;

  Result := true;
end;

// 물리디스크 번호를 얻어온다. 0이상이면 연결된 상태이고 -1이면 없는상태이다
function TTgUSBStorInfo.GetUSBInfoStep_3: Boolean;
var
  pData         : PUSBRec;
  reg           : TRegistry;
//  lstValue    : TStringList;
  i, c, n, nCnt : Integer;
  sVal          : String;
begin
  Result := false;

  Guard(reg, TRegistry.Create);
  reg.RootKey := HKEY_LOCAL_MACHINE;
  if not reg.OpenKeyReadOnly(REG_ENUM_USE_DISK_NUM) then
  begin
//    ASSERT(false);
    exit;
  end;

  if not reg.ValueExists('Count') then
    exit;

  nCnt := reg.ReadInteger('Count');
  for i := 0 to nCnt - 1 do
  begin
    sVal := IntToStr(i);
    if not reg.ValueExists(sVal) then break;
    sVal := reg.ReadString(sVal);

    for c := 0 to USBStorList_.Count - 1 do
    begin
      pData := USBStorList_[c];

    // vista의 경우 serial로 검사~
//      if bIsVista_ then
//      begin
//        n := Pos(pData.sSerial2, sVal);
//      end else
//        n := Pos(pData.sParentIdPrefix, sVal);
      n := Pos(pData.sSerial2, sVal); // 이건 xp나 비스타나 똑같네
      if n <> 0 then
        pData.nDiskNum := i;
    end;
  end;

  Result := true;
end;

// 마운트 정보를 확인해서 드라이브 볼륨명을 구한다.
function TTgUSBStorInfo.GetUSBInfoStep_4: Boolean;
var
  reg                   : TRegistry;
  lstValue              : TStringList;
  i, c                  : Integer;
  sLetter, sValue, sBuf : String;
  pData                 : PUSBRec;
  regInfo               : TRegDataInfo;
  buf                   : array of WideChar;

  function ExtractDriveLetter(const str: String): String;
  var
    nPos: Integer;
  begin
    nPos := LastDelimiter('\', str);
    if nPos = -1 then
    begin
      Result := '';
      exit;
    end;
    Result := UpperCase(Copy(str, nPos+1, Length(str)-nPos));
  end;

begin
  Result := false;

  Guard(reg, TRegistry.Create);
  reg.RootKey := HKEY_LOCAL_MACHINE;
  if not reg.OpenKeyReadOnly(REG_MOUNTED_DEVICES) then
  begin
//    ASSERT(false);
    exit;
  end;

  Guard(lstValue, TStringList.Create);
  reg.GetValueNames(lstValue);

  for i := 0 to lstValue.Count - 1 do
  begin
    sValue := LowerCase(lstValue[i]);
    if Pos('dosdevices', sValue) <> 0 then
    begin
      if not reg.GetDataInfo(sValue, regInfo) then
      begin
//        ASSERT(false);
        exit;
      end;

    // 20보다 작으면 아니라고 보는게 좋다..
    // 외장 하드 디스크의 경우엔 이렇게 나타나는데 다른방식으로 구해주도록 하자 
      if regInfo.DataSize < 20 then
      begin
        sLetter := ExtractDriveLetter(sValue);
        for c := 0 to USBStorList_.Count - 1 do
        begin
          pData := USBStorList_[c];
          if pData.nDiskNum < 0 then
            continue;

          if pData.nDiskNum = GetDriveExtent(sLetter).dwDiskNumber then
          begin
            pData.sDriveLetter := sLetter;
            break;
          end;
        end;
        continue;
      end;

      if regInfo.RegData = rdBinary then
      begin
        SetLength(buf, regInfo.DataSize+1);
        ZeroMemory(buf, regInfo.DataSize+1);
        reg.ReadBinaryData(sValue, buf[0], regInfo.DataSize);
        sBuf := WideCharToString(@buf[0]);
        for c := 0 to USBStorList_.Count - 1 do
        begin
          pData := USBStorList_[c];

          if (Pos(pData.sParentIdPrefix, sBuf) <> 0) or
             (Pos(pData.sSerial2, sBuf) <> 0) then
          begin
            pData.sDriveLetter := ExtractDriveLetter(sValue);
            break;
          end;
        end;
      end;
    end;
  end;

  Result := true;
end;

// 가장 최근에 연결/해제한 날짜를 가져온다.
function TTgUSBStorInfo.GetUSBInfoStep_5: Boolean;
var
  reg     : TRegistry;
  pData   : PUSBRec;
  i       : Integer;
  regInfo : TRegKeyInfo;
  sKey    : String;
begin
  Result := true;

  Guard(reg, TRegistry.Create);
  reg.RootKey := HKEY_LOCAL_MACHINE;

  for i := 0 to Count - 1 do
  begin
    pData := USBStorList_[i];
//    if (bIsVista_ = false) and (pData.sParentIdPrefix = '') then continue;

  // vista와 2000, xp는 다르다!!
    if bIsVista_ then
      sKey := Format(REG_USB_LASTWRITE_CONTROL_VISTA, [pData.sUsbstor, pData.sSerial2])
    else begin
    // 외장 USB하드는 serial이 정보에 포함되지 않기때문에 이렇게 구분
      if (pData.sSerial = '') or (pData.sParentIdPrefix = '') then
        sKey := Format(REG_USB_LASTWRITE_CONTROL1, [pData.sUsbstor, pData.sSerial2])
      else
        sKey := Format(REG_USB_LASTWRITE_CONTROL2, [pData.sParentIdPrefix]);
    end;
    reg.CloseKey;
    if reg.OpenKeyReadOnly(sKey) then
    begin
      if reg.GetKeyInfo(regInfo) then
        pData.dtLastWrite := ConvFileTimeToDateTime_Local(regInfo.FileTime);
    end;
  end;
end;

function TTgUSBStorInfo.UpdateUSBStorInfo: Boolean;
begin
  USBStorList_.Clear;

  Result := GetUSBInfoStep_1;
  if not Result then exit;

  Result := GetUSBInfoStep_2;
  if not Result then exit;

  Result := GetUSBInfoStep_3;
  if not Result then exit;

  Result := GetUSBInfoStep_4;
  if not Result then exit;

  Result := GetUSBInfoStep_5;
  if not Result then exit;
end;

end.
