unit Sparkle.Android.HttpEngine;

{$I Sparkle.Inc}

interface

{$IFDEF ANDROID}

uses
  Generics.Collections, SysUtils, Classes,
  Androidapi.JNI.JavaTypes,
  Sparkle.Android.Import,
  Sparkle.Http.Engine,
  Sparkle.Http.Headers,
  Sparkle.Uri;

type
  TAndroidHttpResponse = class;

  TAndroidHttpResponseStream = class(THttpResponseStream)
  strict private
    FIsError: boolean;
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    FResponse: TAndroidHttpResponse;
    FIntInputStream: JBufferedInputStream;
    FInputClosed: boolean;
    function InputStream: JBufferedInputStream;
  strict protected
    function InternalHttpRead(var Buffer; Count: Longint): Longint;
    function HttpRead(var Buffer; Count: Longint): Longint; override;
  public
    constructor Create(AResponse: TAndroidHttpResponse; AIsError: boolean);
    destructor Destroy; override;
  end;

  TAndroidHttpResponse = class(THttpResponse)
  strict private
    FConn: JHttpURLConnection;
    FContentLength: Int64;
    FContentLengthLoaded: boolean;
    FIsError: Boolean;
  protected
    function GetContentLength: Int64; override;
    function CreateContentStream: TStream; override;
    function GetContentType: string; override;
    function GetContentEncoding: string; override;
    function GetStatusCode: integer; override;
    function GetStatusReason: string; override;
    procedure UpdateHeaders(AHeaders: THttpHeaders); override;
    property HttpConn: JHttpURLConnection read FConn;
  public
    constructor Create(AConn: JHttpURLConnection; AIsError: boolean);
    destructor Destroy; override;
  end;

  TAndroidHttpEngine = class(THttpEngine)
  strict private
    FTunnelUnsupportedMethods: boolean;
  strict protected
    function DoCreateRequest: THttpRequest; override;
    function InternalSend(Request: THttpRequest): THttpResponse;
    function DoSend(Request: THttpRequest): THttpResponse; override;
  public
    constructor Create; override;
    destructor Destroy; override;
    property TunnelUnsupportedMethods: boolean read FTunnelUnsupportedMethods write FTunnelUnsupportedMethods;
  end;

{$ENDIF}

implementation

{$IFDEF ANDROID}

uses
  {$IFDEF DELPHIXE6_LVL}
  Androidapi.Helpers,
  {$ENDIF}
  Androidapi.JNIBridge;

{ TAndroidHttpEngine }

constructor TAndroidHttpEngine.Create;
begin
  FTunnelUnsupportedMethods := true;
end;

function TAndroidHttpEngine.DoCreateRequest: THttpRequest;
begin
  Result := THttpRequest.Create;
end;

function TAndroidHttpEngine.DoSend(Request: THttpRequest): THttpResponse;
var
  LTerminated: Boolean;
  LException: Exception;
  LResponse: THttpResponse;
begin
  // Android doesn't allow network connections in the main thread
  if TThread.CurrentThread.ThreadID = MainThreadID then
  begin
    LTerminated := False;
    LException := nil;
    TThread.CreateAnonymousThread(procedure
      begin
        try
          LResponse := InternalSend(Request);
        except
          on E:Exception do
            LException := E;
        end;
        LTerminated := True;
      end).Start;

    while not LTerminated do
      TThread.Sleep(1);

    if LException <> nil then
      raise LException;
  end
  else
    LResponse := InternalSend(Request);
  Result := LResponse;
end;

destructor TAndroidHttpEngine.Destroy;
begin
  inherited;
end;

function TAndroidHttpEngine.InternalSend(Request: THttpRequest): THttpResponse;
const
  SendBuffer = 8192;
var
  Url: JUrl;
  HttpConn: JHttpURLConnection;
  HeaderInfo: THttpHeaderInfo;
  DataToSend, TotalSent: integer;
  Output: JBufferedOutputStream;
  JavaBytes: TJavaArray<Byte>;
  BufferToSend: PByte;
  Code: integer;
  IsError: boolean;
  UpperMethod: string;
begin
  Url := TJUrl.JavaClass.init(StringToJString(Request.Uri));

  // Create Request
  HttpConn := TJHttpURLConnection.Wrap((Url.openConnection as ILocalObject).GetObjectID);

  // tunnel unsupported http methods
  if Request.Method <> '' then
  begin
    UpperMethod := Uppercase(Request.Method);
    if TunnelUnsupportedMethods and
      (UpperMethod <> '') and
      (UpperMethod <> 'GET') and
      (UpperMethod <> 'POST') and
      (UpperMethod <> 'PUT') and
      (UpperMethod <> 'DELETE') and
      (UpperMethod <> 'HEAD') and
      (UpperMethod <> 'OPTIONS') and
      (UpperMethod <> 'TRACE') then
    begin
      HttpConn.setRequestMethod(StringToJstring('POST'));
      HttpConn.setRequestProperty(
        StringToJstring('x-http-method-override'),
        StringToJstring(Request.Method));
    end
    else
      HttpConn.setRequestMethod(StringToJString(Request.Method));
  end;

  HttpConn.setConnectTimeout(Request.Timeout);
  HttpConn.setReadTimeout(Request.Timeout);
  HttpConn.setInstanceFollowRedirects(false);

  // Set headers
  for HeaderInfo in Request.Headers.AllHeaders do
    HttpConn.setRequestProperty(
      StringToJstring(HeaderInfo.Name),
      StringToJstring(HeaderInfo.Value));

  // make Android client compatible with other platforms:
  // if content-type is not set in the request, client shuold send it blank
  // if we don't do this, NSURLConnection will set it to the default value
  // which is x-www-form-urlencoded
  if not Request.Headers.Exists('content-type') then
    HttpConn.setRequestProperty(
      StringToJstring('content-type'),
      StringToJstring(''));

  // Build http body
  if Request.ContentLength > 0 then
  begin
    HttpConn.setFixedLengthStreamingMode(Request.ContentLength);
    Output := TJBufferedOutputStream.JavaClass.init(HttpConn.getOutputStream);
    JavaBytes := TJavaArray<Byte>.Create(SendBuffer);
    TotalSent := 0;
    BufferToSend := Request.ContentBuffer;
    while TotalSent < Request.ContentLength do
    begin
      DataToSend := Request.ContentLength - TotalSent;
      if DataToSend > SendBuffer then
        DataToSend := SendBuffer;
      Move(BufferToSend^, JavaBytes.Data^, DataToSend);
      Output.write(JavaBytes, 0, DataToSend);
      TotalSent := TotalSent + DataToSend;
      Inc(BufferToSend, DataToSend);
    end;
    Output.close;
  end;

  // retrieve response code and check for results
  Code := HttpConn.getResponseCode;
  IsError := Code >= 400;

  // Create response
  Result := TAndroidHttpResponse.Create(HttpConn, IsError);
end;

{ TMacHttpResponse }

constructor TAndroidHttpResponse.Create(AConn: JHttpURLConnection; AIsError: boolean);
begin
  inherited Create;
  FConn := AConn;
  FIsError := AIsError;
end;

destructor TAndroidHttpResponse.Destroy;
begin
//  FConn.disconnect;
  inherited;
end;

function TAndroidHttpResponse.CreateContentStream: TStream;
begin
  Result := TAndroidHttpResponseStream.Create(Self, FIsError);
end;

function TAndroidHttpResponse.GetContentEncoding: string;
begin
  Result := Headers.Get('content-encoding');
end;

function TAndroidHttpResponse.GetContentLength: Int64;
begin
  if not FContentLengthLoaded then
  begin
    if Chunked then
      FContentLength := -1
    else
      FContentLength := FConn.getContentLength;
    FContentLengthLoaded := true;
  end;
  Result := FContentLength;
end;

function TAndroidHttpResponse.GetContentType: string;
begin
  Result := Headers.Get('content-type');
end;

function TAndroidHttpResponse.GetStatusCode: integer;
begin
  Result := FConn.getResponseCode;
end;

function TAndroidHttpResponse.GetStatusReason: string;
begin
  Result := JStringToString(FConn.getResponseMessage);
end;

procedure TAndroidHttpResponse.UpdateHeaders(AHeaders: THttpHeaders);
var
  HeaderName, HeaderValue: string;
  HeaderMap: JMap;
  Keys: JIterator;
  Key: JObject;
  MapValue, Value: JObject;
  Values: JIterator;
  JHeaderName, JHeaderValue: JString;
begin
  AHeaders.Clear;
  HeaderMap := FConn.getHeaderFields;
  Keys := HeaderMap.keySet.iterator;
  while Keys.hasNext do
  begin
    Key := Keys.next;
    if Key <> nil then
    begin
      MapValue := HeaderMap.get(Key);
      if MapValue <> nil then
      begin
        JHeaderName := TJString.Wrap((Key as ILocalObject).GetObjectID);
        HeaderName := JStringToString(JHeaderName);
        Values := TJList.Wrap((MapValue as ILocalObject).GetObjectID).iterator;
        while Values.hasNext do
        begin
          Value := Values.next;
          if Value <> nil then
          begin
            JHeaderValue := TJString.Wrap((Value as ILocalObject).GetObjectID);
            HeaderValue := JStringToString(JHeaderValue);
            AHeaders.AddValue(HeaderName, HeaderValue);
          end;
        end;
      end;
    end;
  end;
end;

{ TWinHttpResponseStream }

constructor TAndroidHttpResponseStream.Create(AResponse: TAndroidHttpResponse;
  AIsError: boolean);
begin
  inherited Create;
  FIsError := AIsError;
  FResponse := AResponse;
  FInputClosed := false;
end;

destructor TAndroidHttpResponseStream.Destroy;
begin
//  if FIntInputStream <> nil then
//    FIntInputStream.close;
  inherited;
end;

function TAndroidHttpResponseStream.HttpRead(var Buffer; Count: Longint): Longint;
var
  LTerminated: Boolean;
  LException: Exception;
  LReadBytes: Longint;
  BufPointer: Pointer;
begin
  // Android doesn't allow network connections in the main thread
  if TThread.CurrentThread.ThreadID = MainThreadID then
  begin
    LTerminated := False;
    LException := nil;
    BufPointer := @Buffer;
    TThread.CreateAnonymousThread(procedure
      begin
        try
          LReadBytes := InternalHttpRead(BufPointer^, Count)
        except
          on E:Exception do
            LException := E;
        end;
        LTerminated := True;
      end).Start;

    while not LTerminated do
      TThread.Sleep(1);

    if LException <> nil then
      raise LException;
  end
  else
    LReadBytes := InternalHttpRead(Buffer, Count);
  Result := LReadBytes;
end;

function TAndroidHttpResponseStream.InputStream: JBufferedInputStream;
begin
  if FIntInputStream = nil then
  begin
    if FIsError then
      FIntInputStream := TJBufferedInputStream.JavaClass.init(FResponse.HttpConn.getErrorStream)
    else
      FIntInputStream := TJBufferedInputStream.JavaClass.init(FResponse.HttpConn.getInputStream);
  end;
  Result := FIntInputStream;
end;

function TAndroidHttpResponseStream.InternalHttpRead(var Buffer;
  Count: Longint): Longint;
var
  JavaBytes: TJavaArray<Byte>;
  Total, BytesRead: integer;
begin
  if (Count <= 0) or FInputClosed or (InputStream = nil) then Exit(0);

  JavaBytes := TJavaArray<Byte>.Create(Count);
  Total := 0;
  repeat
    BytesRead := InputStream.read(JavaBytes, Total, Count - Total);
    if BytesRead > 0 then
      Total := Total + BytesRead;
  until (BytesRead <= 0) or (Total >= Count);
  if BytesRead <= 0 then
  begin
    FInputClosed := true;
    InputStream.close;
  end;
  Move(JavaBytes.Data^, Buffer, Total);
  Result := Total;
end;

{$ENDIF}

end.

