Subversion Repositories userdetect2

Compare Revisions

No changes between revisions

Regard whitespace Rev 83 → Rev 82

/trunk/UserDetect2/Plugins/TestDynamicEcho.dpr
File deleted
/trunk/UserDetect2/Plugins/TestDynamicEcho.res
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes:
Deleted: svn:mime-type
-application/octet-stream
\ No newline at end of property
/trunk/UserDetect2/Plugins/TestDynamicEcho.dof
File deleted
/trunk/UserDetect2/UD2_Main.dfm
3,7 → 3,7
Top = 177
Width = 784
Height = 440
ActiveControl = Memo1
ActiveControl = TasksListView
Caption = 'ViaThinkSoft UserDetect2'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
22,7 → 22,7
Top = 0
Width = 768
Height = 402
ActivePage = TabSheet5
ActivePage = TasksTabSheet
Align = alClient
TabOrder = 0
object TasksTabSheet: TTabSheet
49,9 → 49,6
object TabSheet2: TTabSheet
Caption = 'Identifications'
ImageIndex = 1
DesignSize = (
760
374)
object IdentificationsListView: TVTSListView
Left = 0
Top = 0
64,9 → 61,6
Width = 100
end
item
Caption = 'Dyn'
end
item
Caption = 'Method name'
Width = 100
end
84,17 → 78,7
ViewStyle = vsReport
OnCompare = ListViewCompare
end
object Button5: TButton
Left = 616
Top = 320
Width = 129
Height = 41
Anchors = [akRight, akBottom]
Caption = 'Test dynamic'
TabOrder = 1
OnClick = Button5Click
end
end
object TabSheet3: TTabSheet
Caption = 'Task Definition File Template'
ImageIndex = 2
619,7 → 603,7
Top = 64
Width = 15
Height = 13
Caption = '2.2'
Caption = '2.1'
end
object Memo1: TMemo
Left = 264
/trunk/UserDetect2/UD2_Main.pas
62,7 → 62,6
MenuItem1: TMenuItem;
Panel2: TPanel;
Image2: TImage;
Button5: TButton;
procedure FormDestroy(Sender: TObject);
procedure TasksListViewDblClick(Sender: TObject);
procedure TasksListViewKeyPress(Sender: TObject; var Key: Char);
79,7 → 78,6
procedure LoadedPluginsPopupMenuPopup(Sender: TObject);
procedure MenuItem1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button5Click(Sender: TObject);
protected
ud2: TUD2;
procedure LoadTaskList;
282,10 → 280,6
with IdentificationsListView.Items.Add do
begin
Caption := pl.PluginName;
if ude.DynamicDataUsed then
SubItems.Add(ude.DynamicData)
else
SubItems.Add('');
SubItems.Add(pl.IdentificationMethodName);
SubItems.Add(ude.IdentificationString);
SubItems.Add(GUIDToString(pl.PluginGUID));
304,7 → 298,6
i, j: integer;
pl: TUD2Plugin;
ude: TUD2IdentificationEntry;
idNames: TStringList;
begin
IniTemplateMemo.Clear;
IniTemplateMemo.Lines.Add('[ExampleTask1]');
324,19 → 317,10
begin
ude := pl.DetectedIdentifications.Items[j] as TUD2IdentificationEntry;
IniTemplateMemo.Lines.Add(Format('; %s', [ude.Plugin.PluginName]));
 
idNames := TStringList.Create;
try
ude.GetIdNames(idNames);
if idNames.Count >= 1 then
IniTemplateMemo.Lines.Add(idNames.Strings[0]+'=calc.exe');
finally
idNames.Free;
IniTemplateMemo.Lines.Add(ude.GetPrimaryIdName+'=calc.exe');
end;
 
end;
end;
end;
 
procedure TUD2MainForm.LoadLoadedPluginList;
resourcestring
567,25 → 551,4
PageControl1.ActivePage := TasksTabSheet;
end;
 
procedure TUD2MainForm.Button5Click(Sender: TObject);
var
idTerm: string;
slCmd: TStrings;
begin
if InputQuery('Enter example term', 'Example: abc|||Testecho:abc=calc.exe', idTerm) then
begin
slCmd := TStringList.Create;
try
ud2.CheckTerm(idTerm, slCmd);
if slCmd.Count = 0 then
ShowMessage('No commands would be executed.')
else
showmessage('Following commands would be executed:' + #13#10#13#10 + slCmd.Text);
finally
slCmd.Free;
end;
end;
LoadDetectedIDs;
end;
 
end.
/trunk/UserDetect2/UD2_Obj.pas
10,16 → 10,12
 
uses
Windows, SysUtils, Classes, IniFiles, Contnrs, Dialogs, UD2_PluginIntf,
UD2_PluginStatus, UD2_Utils;
UD2_PluginStatus;
 
const
cchBufferSize = 32768;
 
dynamicDataDelim = '|||';
 
type
TUD2IdentificationEntry = class;
 
TUD2Plugin = class(TObject)
protected
FDetectedIdentifications: TObjectList{<TUD2IdentificationEntry>};
40,15 → 36,11
 
Time: Cardinal;
function PluginGUIDString: string;
property DetectedIdentifications: TObjectList{<TUD2IdentificationEntry>} read FDetectedIdentifications;
property DetectedIdentifications: TObjectList{<TUD2IdentificationEntry>}
read FDetectedIdentifications;
destructor Destroy; override;
constructor Create;
function AddIdentification(IdStr: WideString): TUD2IdentificationEntry;
 
function InvokeDynamicCheck(dynamicData: string): boolean;
function GetDynamicRequestResult(dynamicData: string): TArrayOfString;
 
function EqualsMethodNameOrGuid(idMethodNameOrGUID: string): boolean;
procedure AddIdentification(IdStr: WideString);
end;
 
TUD2IdentificationEntry = class(TObject)
55,13 → 47,10
private
FIdentificationString: WideString;
FPlugin: TUD2Plugin;
FDynamicDataUsed: boolean;
FDynamicData: string;
public
property DynamicDataUsed: boolean read FDynamicDataUsed write FDynamicDataUsed;
property DynamicData: string read FDynamicData write FDynamicData;
property IdentificationString: WideString read FIdentificationString;
property Plugin: TUD2Plugin read FPlugin;
function GetPrimaryIdName: WideString;
procedure GetIdNames(sl: TStrings);
constructor Create(AIdentificationString: WideString; APlugin: TUD2Plugin);
end;
83,8 → 72,6
property IniFile: TMemIniFile read FIniFile;
procedure GetAllIdNames(outSL: TStrings);
function FulfilsEverySubterm(idTerm: WideString; slIdNames: TStrings=nil): boolean;
procedure CheckTerm(idTermAndCmd: string; commandSLout: TStrings; slIdNames: TStrings=nil);
function FindPluginByMethodNameOrGuid(idMethodName: string): TUD2Plugin;
procedure GetCommandList(ShortTaskName: string; outSL: TStrings);
procedure HandlePluginDir(APluginDir, AFileMask: string);
procedure GetTaskListing(outSL: TStrings);
100,7 → 87,7
implementation
 
uses
Math;
UD2_Utils;
 
type
TUD2PluginLoader = class(TThread)
107,15 → 94,12
protected
dllFile: string;
lngID: LANGID;
useDynamicData: boolean;
dynamicData: WideString;
procedure Execute; override;
function HandleDLL: boolean;
public
pl: TUD2Plugin; // TODO: why do we need it?! can it be leaked if we use it for dynamic requests?
pl: TUD2Plugin;
Errors: TStringList;
ResultIdentifiers: TArrayOfString;
constructor Create(Suspended: boolean; DLL: string; alngid: LANGID; useDynamicData: boolean; dynamicData: WideString);
constructor Create(Suspended: boolean; DLL: string; alngid: LANGID);
destructor Destroy; override;
end;
 
131,7 → 115,6
LNG_STATUS_NOTAVAIL_HW_NOT_SUPPORTED = 'Not available (Hardware not supported)';
LNG_STATUS_NOTAVAIL_NO_ENTITIES = 'Not available (No entities to identify)';
LNG_STATUS_NOTAVAIL_WINAPI_CALL_FAILURE = 'Not available (A Windows API call failed. Message: %s)';
LNG_STATUS_NOTAVAIL_ONLY_ACCEPT_DYNAMIC = 'Not available (Arguments required)';
LNG_UNKNOWN_NOTAVAIL = 'Not available (Unknown status code %s)';
 
LNG_STATUS_FAILURE_UNSPECIFIED = 'Error (Unspecified)';
153,7 → 136,6
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_NOTAVAIL_HW_NOT_SUPPORTED, false) then result := LNG_STATUS_NOTAVAIL_HW_NOT_SUPPORTED
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_NOTAVAIL_NO_ENTITIES, false) then result := LNG_STATUS_NOTAVAIL_NO_ENTITIES
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_NOTAVAIL_WINAPI_CALL_FAILURE, false) then result := Format(LNG_STATUS_NOTAVAIL_WINAPI_CALL_FAILURE, [FormatOSError(grStatus.dwExtraInfo)])
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_NOTAVAIL_ONLY_ACCEPT_DYNAMIC, false) then result := LNG_STATUS_NOTAVAIL_ONLY_ACCEPT_DYNAMIC
 
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_FAILURE_UNSPECIFIED, false) then result := LNG_STATUS_FAILURE_UNSPECIFIED
else if UD2_STATUS_Equal(grStatus, UD2_STATUS_FAILURE_BUFFER_TOO_SMALL, false) then result := LNG_STATUS_FAILURE_BUFFER_TOO_SMALL
175,10 → 157,9
result := UpperCase(GUIDToString(PluginGUID));
end;
 
function TUD2Plugin.AddIdentification(IdStr: WideString): TUD2IdentificationEntry;
procedure TUD2Plugin.AddIdentification(IdStr: WideString);
begin
result := TUD2IdentificationEntry.Create(IdStr, Self);
DetectedIdentifications.Add(result);
DetectedIdentifications.Add(TUD2IdentificationEntry.Create(IdStr, Self))
end;
 
destructor TUD2Plugin.Destroy;
193,77 → 174,19
FDetectedIdentifications := TObjectList{<TUD2IdentificationEntry>}.Create(true);
end;
 
function TUD2Plugin.InvokeDynamicCheck(dynamicData: string): boolean;
var
ude: TUD2IdentificationEntry;
i: integer;
ids: TArrayOfString;
id: string;
begin
result := false;
{ TUD2IdentificationEntry }
 
for i := 0 to FDetectedIdentifications.Count-1 do
function TUD2IdentificationEntry.GetPrimaryIdName: WideString;
begin
ude := FDetectedIdentifications.Items[i] as TUD2IdentificationEntry;
if ude.dynamicDataUsed and (ude.dynamicData = dynamicData) then
begin
// The dynamic content was already evaluated (and therefore is already added in FDetectedIdentifications).
Exit;
result := Plugin.IdentificationMethodName+':'+IdentificationString;
end;
end;
 
SetLength(ids, 0);
ids := GetDynamicRequestResult(dynamicData);
 
for i := 0 to Length(ids)-1 do
begin
id := ids[i];
 
ude := AddIdentification(id);
ude.dynamicDataUsed := true;
ude.dynamicData := dynamicData;
 
result := true;
end;
end;
 
function TUD2Plugin.GetDynamicRequestResult(dynamicData: string): TArrayOfString;
var
lngID: LANGID;
pll: TUD2PluginLoader;
begin
lngID := GetSystemDefaultLangID;
 
pll := TUD2PluginLoader.Create(false, PluginDLL, lngid, true, dynamicData);
try
pll.WaitFor;
result := pll.ResultIdentifiers;
finally
pll.Free;
end;
end;
 
function TUD2Plugin.EqualsMethodNameOrGuid(idMethodNameOrGUID: string): boolean;
begin
result := SameText(IdentificationMethodName, idMethodNameOrGUID) or
SameText(GUIDToString(PluginGUID), idMethodNameOrGUID)
end;
 
{ TUD2IdentificationEntry }
 
procedure TUD2IdentificationEntry.GetIdNames(sl: TStrings);
begin
if DynamicDataUsed then
begin
sl.Add(DynamicData+dynamicDataDelim+Plugin.IdentificationMethodName+':'+IdentificationString);
sl.Add(DynamicData+DynamicDataDelim+Plugin.PluginGUIDString+':'+IdentificationString);
end
else
begin
sl.Add(GetPrimaryIdName);
sl.Add(Plugin.IdentificationMethodName+':'+IdentificationString);
sl.Add(Plugin.PluginGUIDString+':'+IdentificationString);
end;
end;
 
constructor TUD2IdentificationEntry.Create(AIdentificationString: WideString;
APlugin: TUD2Plugin);
303,7 → 226,7
try
repeat
try
tob.Add(TUD2PluginLoader.Create(false, path + sr.Name, lngid, false, ''));
tob.Add(TUD2PluginLoader.Create(false, path + sr.Name, lngid));
except
on E: Exception do
begin
416,14 → 339,13
 
(*
 
NAMING EXAMPLE: dynXYZ|||ComputerName:ABC&&User:John=calc.exe
NAMING EXAMPLE: ComputerName:ABC&&User:John=calc.exe
 
idTerm: dynXYZ|||ComputerName:ABC&&User:John
idTerm: ComputerName:ABC&&User:John
idName: ComputerName:ABC
IdMethodName: ComputerName
IdStr ABC
cmd: calc.exe
dynamicData: dynXYZ
 
*)
 
448,14 → 370,11
const
CASE_SENSITIVE_FLAG = '$CASESENSITIVE$';
var
x, y, z: TArrayOfString;
x: TArrayOfString;
i: integer;
p: TUD2Plugin;
idName: WideString;
cleanUpStringList: boolean;
caseSensitive: boolean;
dynamicData: string;
idMethodName: string;
begin
cleanUpStringList := slIdNames = nil;
try
468,7 → 387,6
SetLength(x, 0);
if Pos(':', idTerm) = 0 then
begin
// Exclude stuff like "Description"
result := false;
Exit;
end;
478,33 → 396,6
begin
idName := x[i];
 
/// --- Start Dynamic Extension
 
SetLength(y, 0);
y := SplitString(dynamicDataDelim, idName);
 
if Length(y) >= 2 then
begin
dynamicData := y[0];
 
SetLength(z, 0);
z := SplitString(':', y[1]);
idMethodName := z[0];
 
p := FindPluginByMethodNameOrGuid(idMethodName);
if Assigned(p) then
begin
if p.InvokeDynamicCheck(dynamicData) then
begin
// Reload the identifications
slIdNames.Clear;
GetAllIdNames(slIdNames);
end;
end;
end;
 
/// --- End Dynamic Extension
 
if Pos(CASE_SENSITIVE_FLAG, idName) >= 1 then
begin
idName := StringReplace(idName, CASE_SENSITIVE_FLAG, '', [rfReplaceAll]);
528,29 → 419,16
end;
end;
 
function TUD2.FindPluginByMethodNameOrGuid(idMethodName: string): TUD2Plugin;
var
i: integer;
p: TUD2Plugin;
begin
result := nil;
for i := 0 to LoadedPlugins.Count-1 do
begin
p := LoadedPlugins.Items[i] as TUD2Plugin;
 
if p.EqualsMethodNameOrGuid(idMethodName) then
begin
result := p;
Exit;
end;
end;
end;
 
procedure TUD2.GetCommandList(ShortTaskName: string; outSL: TStrings);
var
i: integer;
cmd: string;
idTerm: WideString;
slSV, slIdNames: TStrings;
nameVal: TArrayOfString;
begin
SetLength(nameVal, 0);
 
slIdNames := TStringList.Create;
try
GetAllIdNames(slIdNames);
560,47 → 438,22
FIniFile.ReadSectionValues(ShortTaskName, slSV);
for i := 0 to slSV.Count-1 do
begin
CheckTerm(slSV.Strings[i], outSL, slIdNames);
end;
finally
slSV.Free;
end;
finally
slIdNames.Free;
end;
end;
 
procedure TUD2.CheckTerm(idTermAndCmd: string; commandSLout: TStrings; slIdNames: TStrings=nil);
var
nameVal: TArrayOfString;
idTerm, cmd: string;
slIdNamesCreated: boolean;
begin
slIdNamesCreated := false;
try
if not Assigned(slIdNames) then
begin
slIdNamesCreated := true;
slIdNames := TStringList.Create;
GetAllIdNames(slIdNames);
end;
 
SetLength(nameVal, 0);
 
// We are doing the interpretation of the line ourselves, because
// TStringList.Values[] would not allow multiple command lines with the
// same key (idTerm)
// TODO xxx: big problem when we want to check environment variables, since our idTerm would contain '=' !
nameVal := SplitString('=', idTermAndCmd);
if Length(nameVal) < 2 then exit;
nameVal := SplitString('=', slSV.Strings[i]);
idTerm := nameVal[0];
cmd := nameVal[1];
 
if FulfilsEverySubterm(idTerm, slIdNames) then commandSLout.Add(cmd);
if FulfilsEverySubterm(idTerm, slIdNames) then outSL.Add(cmd);
end;
finally
if slIdNamesCreated then slIdNames.Free;
slSV.Free;
end;
finally
slIdNames.Free;
end;
end;
 
{ TUD2PluginLoader }
 
611,7 → 464,7
HandleDLL;
end;
 
constructor TUD2PluginLoader.Create(Suspended: boolean; DLL: string; alngid: LANGID; useDynamicData: boolean; dynamicData: WideString);
constructor TUD2PluginLoader.Create(Suspended: boolean; DLL: string; alngid: LANGID);
begin
inherited Create(Suspended);
dllfile := dll;
618,8 → 471,6
pl := nil;
Errors := TStringList.Create;
lngid := alngid;
self.useDynamicData := useDynamicData;
Self.dynamicData := dynamicData;
end;
 
destructor TUD2PluginLoader.Destroy;
631,6 → 482,7
function TUD2PluginLoader.HandleDLL: boolean;
var
sIdentifier: WideString;
sIdentifiers: TArrayOfString;
buf: array[0..cchBufferSize-1] of WideChar;
pluginInterfaceID: TGUID;
dllHandle: Cardinal;
641,7 → 493,6
fPluginVersionW: TFuncPluginVersionW;
fIdentificationMethodNameW: TFuncIdentificationMethodNameW;
fIdentificationStringW: TFuncIdentificationStringW;
fDynamicIdentificationStringW: TFuncDynamicIdentificationStringW;
fCheckLicense: TFuncCheckLicense;
fDescribeOwnStatusCodeW: TFuncDescribeOwnStatusCodeW;
statusCode: UD2_STATUS;
653,7 → 504,6
function _ErrorLookup(statusCode: UD2_STATUS): WideString;
var
ret: BOOL;
buf: array[0..cchBufferSize-1] of WideChar;
begin
if Assigned(fDescribeOwnStatusCodeW) then
begin
770,22 → 620,6
Exit;
end;
 
fDynamicIdentificationStringW := nil;
fIdentificationStringW := nil;
if useDynamicData then
begin
@fDynamicIdentificationStringW := GetProcAddress(dllHandle, mnDynamicIdentificationStringW);
if not Assigned(fDynamicIdentificationStringW) then
begin
// TODO xxx: Darf hier ein fataler Fehler entstehen, obwohl dieses Szenario nur durch die INI file auftreten kann?
// TODO (allgemein): In der Modulübersicht soll auch gezeigt werden, ob dieses Modul dynamischen Content erlaubt.
// TODO (allgemein): doku
Errors.Add(Format(LNG_METHOD_NOT_FOUND, [mnDynamicIdentificationStringW, dllFile]));
Exit;
end;
end
else
begin
@fIdentificationStringW := GetProcAddress(dllHandle, mnIdentificationStringW);
if not Assigned(fIdentificationStringW) then
begin
792,7 → 626,6
Errors.Add(Format(LNG_METHOD_NOT_FOUND, [mnIdentificationStringW, dllFile]));
Exit;
end;
end;
 
@fPluginNameW := GetProcAddress(dllHandle, mnPluginNameW);
if not Assigned(fPluginNameW) then
899,14 → 732,7
 
ZeroMemory(@buf, cchBufferSize);
statusCode := UD2_STATUS_FAILURE_NO_RETURNED_VALUE; // This status will be used when the DLL does not return anything (which is an error by the developer)
if useDynamicData then
begin
statusCode := fDynamicIdentificationStringW(@buf, cchBufferSize, PWideChar(dynamicData));
end
else
begin
statusCode := fIdentificationStringW(@buf, cchBufferSize);
end;
pl.IdentificationProcedureStatusCode := statusCode;
pl.IdentificationProcedureStatusCodeDescribed := _ErrorLookup(statusCode);
if statusCode.wCategory = UD2_STATUSCAT_SUCCESS then
915,19 → 741,16
if UD2_STATUS_Equal(statusCode, UD2_STATUS_OK_MULTILINE, false) then
begin
// Multiple identifiers (e.g. multiple MAC addresses are delimited via UD2_MULTIPLE_ITEMS_DELIMITER)
SetLength(ResultIdentifiers, 0);
ResultIdentifiers := SplitString(UD2_MULTIPLE_ITEMS_DELIMITER, sIdentifier);
for i := Low(ResultIdentifiers) to High(ResultIdentifiers) do
SetLength(sIdentifiers, 0);
sIdentifiers := SplitString(UD2_MULTIPLE_ITEMS_DELIMITER, sIdentifier);
for i := Low(sIdentifiers) to High(sIdentifiers) do
begin
pl.AddIdentification(ResultIdentifiers[i]);
pl.AddIdentification(sIdentifiers[i]);
end;
end
else
begin
pl.AddIdentification(sIdentifier);
 
SetLength(ResultIdentifiers, 1);
ResultIdentifiers[0] := sIdentifier;
end;
end
else if statusCode.wCategory <> UD2_STATUSCAT_NOT_AVAIL then
/trunk/UserDetect2/UD2_PluginIntf.pas
22,7 → 22,6
mnIdentificationMethodNameW = 'IdentificationMethodNameW';
mnIdentificationStringW = 'IdentificationStringW';
mnDescribeOwnStatusCodeW = 'DescribeOwnStatusCodeW';
mnDynamicIdentificationStringW = 'DynamicIdentificationStringW';
 
{$IF not Declared(LPVOID)}
type
40,12 → 39,6
TFuncIdentificationStringW = function(lpIdentifier: LPWSTR; cchSize: DWORD): UD2_STATUS; cdecl;
TFuncDescribeOwnStatusCodeW = function(lpErrorDescription: LPWSTR; cchSize: DWORD; statusCode: UD2_STATUS; wLangID: LANGID): BOOL; cdecl;
 
// Extension of the plugin API starting with version 3.
// We don't assign a new PluginIdentifier GUID since the methods of the old API
// are still valid, so an UserDetect2 2.x plugin can be still used with UserDetect2 3.x.
// Therefore, this function *MUST* be optional and therefore it may only be imported dynamically.
TFuncDynamicIdentificationStringW = function(lpIdentifier: LPWSTR; cchSize: DWORD; lpDynamicData: LPWSTR): UD2_STATUS; cdecl;
 
const
UD2_MULTIPLE_ITEMS_DELIMITER = #10;
 
/trunk/UserDetect2/UD2_PluginStatus.pas
108,14 → 108,6
dwMessage: 4;
dwExtraInfo: 0
);
UD2_STATUS_NOTAVAIL_ONLY_ACCEPT_DYNAMIC: UD2_STATUS = (
cbSize: SizeOf(UD2_STATUS);
bReserved: 0;
wCategory: UD2_STATUSCAT_NOT_AVAIL;
grAuthority: UD2_STATUSAUTH_GENERIC_;
dwMessage: 5;
dwExtraInfo: 0
);
 
(* Failure codes *)
 
/trunk/vcl/PatchU.pas
1,5 → 1,8
unit PatchU;
 
{$WARN UNSAFE_CODE OFF}
{$WARN UNSAFE_TYPE OFF}
 
interface
 
type
/trunk/vcl/VTSCompat.pas
4,6 → 4,10
{$LEGACYIFEND ON}
{$IFEND}
 
{$WARN UNSAFE_CODE OFF}
{$WARN UNSAFE_TYPE OFF}
{$WARN UNSAFE_CAST OFF}
 
interface
 
uses
/trunk/vcl/VTSListView.pas
1,5 → 1,8
unit VTSListView;
 
{$WARN UNSAFE_CODE OFF}
{$WARN UNSAFE_TYPE OFF}
 
interface
 
// This ListView adds support for sorting arrows