Garmin: Handle http requests async

This commit is contained in:
José Rebelo
2026-01-22 19:49:48 +01:00
committed by José Rebelo
parent b8a0c0dbbb
commit 678ef35904
19 changed files with 354 additions and 349 deletions
@@ -6,5 +6,5 @@ import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback;
interface IHttpService {
int version();
void get(in HttpRequest request, IHttpCallback cb);
void send(in HttpRequest request, IHttpCallback cb);
}
@@ -5,5 +5,4 @@ import nodomain.freeyourgadget.internethelper.aidl.wifi.IWifiCallback;
interface IWifiService {
int version();
String getCurrentSsid();
void connect(String ssid, String password, IWifiCallback cb);
}
@@ -169,7 +169,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
"GET",
Collections.emptyMap(),
null,
"application/json",
false
);
JSONObject appEntry = (JSONObject) appstoreEntry.getJSONArray("data").get(0);
@@ -909,7 +909,7 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
}
if (headers == null) headers = emptyMap();
String response = InternetUtils.Companion.doStringRequest(Uri.parse(url), method, headers, body, "application/json", insecure);
String response = InternetUtils.Companion.doStringRequest(Uri.parse(url), method, headers, body, insecure);
JSONObject o = new JSONObject();
String _xmlPath = "";
String _xmlReturn = "";
@@ -91,6 +91,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.IncomingFitDefinitionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.MaxPacketSizeDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.ProtobufResponseEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitAsyncProcessor;
@@ -407,6 +408,14 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC
.build());
sendOutgoingMessage("init realtime settings", realtimeSettingsInit);
}
} else if (deviceEvent instanceof ProtobufResponseEvent protobufResponseEvent) {
sendOutgoingMessage(
"protobuf response event for " + protobufResponseEvent.messageId,
protocolBufferHandler.prepareProtobufResponse(
protobufResponseEvent.payload,
protobufResponseEvent.messageId
)
);
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable);
@@ -56,8 +56,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHand
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
import nodomain.freeyourgadget.gadgetbridge.util.notifications.GBProgressNotification;
import nodomain.freeyourgadget.gadgetbridge.webview.CurrentPosition;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -133,11 +131,12 @@ public class ProtocolBufferHandler implements MessageHandler {
return prepareProtobufResponse(processProtobufSmsNotificationMessage(smart.getSmsNotificationService()), message.getRequestId());
}
if (smart.hasHttpService()) {
final GdiHttpService.HttpService response = httpHandler.handle(smart.getHttpService());
if (response == null) {
return null;
final GdiHttpService.HttpService response = httpHandler.handle(smart.getHttpService(), message.getRequestId());
if (response != null) {
return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setHttpService(response).build(), message.getRequestId());
}
return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setHttpService(response).build(), message.getRequestId());
processed = true;
// Response will be async
}
if (smart.hasDataTransferService()) {
final GdiDataTransferService.DataTransferService response = dataTransferHandler.handle(smart.getDataTransferService(), message.getRequestId());
@@ -619,7 +618,7 @@ public class ProtocolBufferHandler implements MessageHandler {
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_REQUEST, requestId);
}
private ProtobufMessage prepareProtobufResponse(GdiSmartProto.Smart protobufPayload, int requestId) {
public ProtobufMessage prepareProtobufResponse(GdiSmartProto.Smart protobufPayload, int requestId) {
if (null == protobufPayload)
return null;
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_RESPONSE, requestId);
@@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
import android.content.Context;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
public class ProtobufResponseEvent extends GBDeviceEvent {
public GdiSmartProto.Smart payload;
public int messageId;
public ProtobufResponseEvent(final GdiSmartProto.Smart payload, final int messageId) {
this.payload = payload;
this.messageId = messageId;
}
@Override
public void evaluate(@NonNull final Context context, @NonNull final GBDevice device) {
// Handled in support class
}
}
@@ -18,11 +18,15 @@ public class GarminHttpRequest {
private final GdiHttpService.HttpService.RawRequest rawRequest;
private final GdiHttpService.HttpService.WebRequest webRequest;
private final int messageRequestId;
private final String method;
private final Uri uri;
private final Map<String, String> headers;
public GarminHttpRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
public GarminHttpRequest(final GdiHttpService.HttpService.RawRequest rawRequest,
final int messageRequestId) {
this.messageRequestId = messageRequestId;
this.rawRequest = rawRequest;
this.webRequest = null;
this.method = rawRequest.getMethod().name();
@@ -30,7 +34,9 @@ public class GarminHttpRequest {
this.headers = headersToMap(rawRequest.getHeaderList());
}
public GarminHttpRequest(final GdiHttpService.HttpService.WebRequest webRequest) throws GarminJsonException {
public GarminHttpRequest(final GdiHttpService.HttpService.WebRequest webRequest,
final int messageRequestId) throws GarminJsonException {
this.messageRequestId = messageRequestId;
this.rawRequest = null;
this.webRequest = webRequest;
this.method = webRequest.getMethod().name();
@@ -44,10 +50,18 @@ public class GarminHttpRequest {
}
}
public int getMessageRequestId() {
return messageRequestId;
}
public GdiHttpService.HttpService.RawRequest getRawRequest() {
return rawRequest;
}
public GdiHttpService.HttpService.WebRequest getWebRequest() {
return webRequest;
}
public Uri getUri() {
return uri;
}
@@ -5,6 +5,7 @@ import java.util.Map;
import java.util.concurrent.Callable;
public class GarminHttpResponse {
private boolean complete = true;
private int status = 200;
private final Map<String, String> headers = new LinkedHashMap<>();
private byte[] body = new byte[0];
@@ -14,6 +15,14 @@ public class GarminHttpResponse {
public GarminHttpResponse() {
}
public boolean isComplete() {
return complete;
}
public void setComplete(final boolean complete) {
this.complete = complete;
}
public int getStatus() {
return status;
}
@@ -51,9 +51,9 @@ public class HttpHandler {
);
}
public GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) {
public GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService, final int messageRequestId) {
if (httpService.hasRawRequest()) {
final GdiHttpService.HttpService.RawResponse rawResponse = handleRawRequest(httpService.getRawRequest());
final GdiHttpService.HttpService.RawResponse rawResponse = handleRawRequest(httpService.getRawRequest(), messageRequestId);
if (rawResponse != null) {
return GdiHttpService.HttpService.newBuilder()
.setRawResponse(rawResponse)
@@ -61,7 +61,7 @@ public class HttpHandler {
}
return null;
} else if (httpService.hasWebRequest()) {
final GdiHttpService.HttpService.WebResponse webResponse = handleWebRequest(httpService.getWebRequest());
final GdiHttpService.HttpService.WebResponse webResponse = handleWebRequest(httpService.getWebRequest(), messageRequestId);
if (webResponse != null) {
return GdiHttpService.HttpService.newBuilder()
.setWebResponse(webResponse)
@@ -75,10 +75,11 @@ public class HttpHandler {
return null;
}
public GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), rawRequest.getUrl());
public GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest,
final int messageRequestId) {
LOG.debug("Got rawRequest {}: {} - {}", messageRequestId, rawRequest.getMethod(), rawRequest.getUrl());
final GarminHttpRequest request = new GarminHttpRequest(rawRequest);
final GarminHttpRequest request = new GarminHttpRequest(rawRequest, messageRequestId);
final GarminHttpResponse response = handleRequest(request);
@@ -88,6 +89,11 @@ public class HttpHandler {
.build();
}
if (!response.isComplete()) {
// Async response
return null;
}
return createRawResponse(request, response);
}
@@ -106,14 +112,19 @@ public class HttpHandler {
LOG.debug("Handling request to {}", interceptor.getClass().getSimpleName());
final GarminHttpResponse response = interceptor.handle(request);
if (response != null) {
LOG.debug("Response from interceptor: {}, {} bytes", response.getStatus(), response.getBody().length);
LOG.debug(
"Response from interceptor: {}, {} bytes, complete={}",
response.getStatus(),
response.getBody().length,
response.isComplete()
);
} else {
LOG.debug("No response from interceptor");
}
return response;
}
private static GdiHttpService.HttpService.RawResponse createRawResponse(
public static GdiHttpService.HttpService.RawResponse createRawResponse(
final GarminHttpRequest request,
final GarminHttpResponse response
) {
@@ -178,11 +189,12 @@ public class HttpHandler {
.build();
}
public GdiHttpService.HttpService.WebResponse handleWebRequest(final GdiHttpService.HttpService.WebRequest webRequest) {
LOG.debug("Got webRequest: {} {}", webRequest.getMethod(), webRequest.getUrl());
public GdiHttpService.HttpService.WebResponse handleWebRequest(final GdiHttpService.HttpService.WebRequest webRequest,
final int messageRequestId) {
LOG.debug("Got webRequest {}: {} {}", messageRequestId, webRequest.getMethod(), webRequest.getUrl());
try {
final GarminHttpRequest request = new GarminHttpRequest(webRequest);
final GarminHttpRequest request = new GarminHttpRequest(webRequest, messageRequestId);
final GarminHttpResponse response = handleRequest(request);
@@ -193,6 +205,69 @@ public class HttpHandler {
.build();
}
if (!response.isComplete()) {
// Async response
return null;
}
return createWebResponse(request, response);
} catch (final Exception e) {
LOG.error("Failed to create web response", e);
return GdiHttpService.HttpService.WebResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.UNKNOWN_STATUS)
.setHttpStatus(0)
.build();
}
}
public static GdiHttpService.HttpService createSuccessResponse(final GarminHttpRequest request,
final GarminHttpResponse response) {
if (request.getRawRequest() != null) {
final GdiHttpService.HttpService.RawResponse rawResponse = createRawResponse(request, response);
if (rawResponse != null) {
return GdiHttpService.HttpService.newBuilder()
.setRawResponse(rawResponse)
.build();
}
return null;
} else if (request.getWebRequest() != null) {
final GdiHttpService.HttpService.WebResponse webResponse = createWebResponse(request, response);
if (webResponse != null) {
return GdiHttpService.HttpService.newBuilder()
.setWebResponse(webResponse)
.build();
}
return null;
} else {
throw new IllegalArgumentException("Should never happen");
}
}
public static GdiHttpService.HttpService createErrorResponse(final GarminHttpRequest request) {
if (request.getRawRequest() != null) {
return GdiHttpService.HttpService.newBuilder()
.setRawResponse(GdiHttpService.HttpService.RawResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.UNKNOWN_STATUS)
.build())
.build();
} else if (request.getWebRequest() != null) {
return GdiHttpService.HttpService.newBuilder()
.setWebResponse(GdiHttpService.HttpService.WebResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.UNKNOWN_STATUS)
.setHttpStatus(0)
.build())
.build();
} else {
throw new IllegalArgumentException("Should never happen");
}
}
public static GdiHttpService.HttpService.WebResponse createWebResponse(
final GarminHttpRequest request,
final GarminHttpResponse response
) {
final GdiHttpService.HttpService.WebRequest webRequest = request.getWebRequest();
try {
final byte[] headers;
if (webRequest.getHttpHeadersInResponse()) {
byte[] encodedHeaders = null;
@@ -1,7 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.interceptors;
import android.os.RemoteException;
import android.webkit.WebResourceResponse;
import android.os.ParcelFileDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -11,24 +10,31 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.internet.InternetFirewall;
import nodomain.freeyourgadget.gadgetbridge.internet.InternetRequestType;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.ProtobufResponseEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.GarminHttpRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.GarminHttpResponse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHandler;
import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton;
import nodomain.freeyourgadget.internethelper.aidl.http.HttpRequest;
import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse;
import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback;
import nodomain.freeyourgadget.internethelper.aidl.http.IHttpService;
@SuppressWarnings("ClassCanBeRecord")
public class FirewallInterceptor implements HttpInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(FirewallInterceptor.class);
private final InternetFirewall firewall;
private final GarminSupport deviceSupport;
public FirewallInterceptor(final GarminSupport deviceSupport) {
this.deviceSupport = deviceSupport;
this.firewall = new InternetFirewall(InternetRequestType.WATCH_APP, deviceSupport.getDevice());
}
@@ -59,44 +65,90 @@ public class FirewallInterceptor implements HttpInterceptor {
return null;
}
// TODO refactor this to be asynchronous
final WebResourceResponse response;
try {
response = InternetHelperSingleton.INSTANCE.send(
request.getUri(),
HttpRequest.Method.valueOf(request.getMethod()),
request.getHeaders(),
new String(request.getBody(), StandardCharsets.UTF_8), // FIXME we should be able to send the raw bytes
"application/json",
false
);
} catch (final RemoteException e) {
LOG.error("Remote exception", e);
return null;
} catch (final InterruptedException e) {
LOG.error("Request was interrupted", e);
final IHttpService httpService = InternetHelperSingleton.INSTANCE.getHttpService();
if (httpService == null) {
LOG.error("HttpService is null");
return null;
}
if (response == null) {
LOG.error("Response is null");
LOG.debug("Forwarding http request {} to InternetHelper", request.getMessageRequestId());
try {
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
final ParcelFileDescriptor pipeRead = pipe[0];
final ParcelFileDescriptor pipeWrite = pipe[1];
final HttpRequest httpRequest = new HttpRequest(
request.getUri().toString(),
HttpRequest.Method.valueOf(request.getMethod()),
request.getHeaders(),
pipeRead,
false
);
httpService.send(httpRequest, new IHttpCallback.Stub() {
@Override
public void onResponse(final HttpResponse response) {
LOG.debug("Got response to {} from InternetHelper", request.getMessageRequestId());
handleHttpResponse(request, response);
}
@Override
public void onException(final String message) {
LOG.error("Http request for {} failed: {}", request.getMessageRequestId(), message);
handleHttpException(request);
}
});
try (ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(pipeWrite)) {
out.write(request.getBody());
}
} catch (final Exception e) {
LOG.error("Failed to send request to InternetHelper", e);
return null;
}
final GarminHttpResponse garminHttpResponse = new GarminHttpResponse();
garminHttpResponse.setStatus(response.getStatusCode());
garminHttpResponse.getHeaders().putAll(response.getResponseHeaders());
try {
garminHttpResponse.setBody(readAllBytes(response.getData()));
} catch (final IOException e) {
LOG.error("Failed to read bytes from response", e);
return null;
}
garminHttpResponse.setComplete(false);
return garminHttpResponse;
}
private void handleHttpResponse(@NotNull final GarminHttpRequest request,
final HttpResponse response) {
final GarminHttpResponse garminHttpResponse = new GarminHttpResponse();
garminHttpResponse.setStatus(response.getStatus());
garminHttpResponse.getHeaders().putAll(response.getHeaders());
try (ParcelFileDescriptor.AutoCloseInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(response.getBody())) {
garminHttpResponse.setBody(readAllBytes(in));
} catch (final IOException e) {
LOG.error("Failed to read bytes from response", e);
handleHttpException(request);
return;
}
final GdiHttpService.HttpService successResponse = HttpHandler.createSuccessResponse(request, garminHttpResponse);
if (successResponse != null) {
sendHttpServiceRequest(request, successResponse);
} else {
final GdiHttpService.HttpService errorResponse = HttpHandler.createErrorResponse(request);
sendHttpServiceRequest(request, errorResponse);
}
}
private void handleHttpException(@NotNull final GarminHttpRequest request) {
final GdiHttpService.HttpService errorResponse = HttpHandler.createErrorResponse(request);
sendHttpServiceRequest(request, errorResponse);
}
private void sendHttpServiceRequest(@NotNull final GarminHttpRequest request,
@NotNull final GdiHttpService.HttpService httpService) {
final GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder().setHttpService(httpService).build();
deviceSupport.evaluateGBDeviceEvent(new ProtobufResponseEvent(
smart, request.getMessageRequestId()
));
}
public static byte[] readAllBytes(final InputStream in) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
@@ -27,7 +27,6 @@ import android.os.ParcelFileDescriptor
import android.os.RemoteException
import android.webkit.WebResourceResponse
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders
import nodomain.freeyourgadget.internethelper.aidl.http.HttpRequest
import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse
import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback
@@ -58,7 +57,6 @@ object InternetHelperSingleton {
}
fun getHttpService(): IHttpService? {
ensureInternetHelperBound()
return internetHelper
}
@@ -89,50 +87,45 @@ object InternetHelperSingleton {
fun send(
webRequest: Uri,
method: HttpRequest.Method,
requestHeaders: Map<String, String>,
body: String?,
bodyContentType: String,
allowInsecure: Boolean,
requestHeaders: Map<String, String> = emptyMap(),
body: ByteArray?,
allowInsecure: Boolean = false,
): WebResourceResponse? {
if (internetHelper == null) {
LOG.error("Internet helper is not available")
return null
}
val latch = CountDownLatch(1)
var result: WebResourceResponse? = null
val request = HttpRequest(webRequest.toString(), method, body, bodyContentType, allowInsecure, HttpHeaders(requestHeaders))
// Create pipe to stream request body
val pipeRead: ParcelFileDescriptor?
val pipeWrite: ParcelFileDescriptor?
if (body != null) {
val pipes = ParcelFileDescriptor.createPipe()
pipeRead = pipes[0]
pipeWrite = pipes[1]
} else {
pipeRead = null
pipeWrite = null
}
val request = HttpRequest(
webRequest.toString(),
method,
requestHeaders,
pipeRead,
allowInsecure,
)
LOG.debug("Forwarding {} request to {} to internet helper app", method.name, webRequest)
try {
internetHelper?.get(request, object : IHttpCallback.Stub() {
internetHelper?.send(request, object : IHttpCallback.Stub() {
override fun onResponse(response: HttpResponse) {
try {
val headers = response.headers.toMap()
val contentType = response.headers["content-type"]
?.substringBefore(";")
?.trim()
?: "application/octet-stream"
val encoding = response.headers["content-encoding"] ?: "UTF-8"
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
result = if (contentType.equals("text/html", ignoreCase = true)) {
val rawHtml = inputStream.bufferedReader(Charset.forName(encoding)).use { it.readText() }
val cleanedHtml = Jsoup.parse(rawHtml).html()
WebResourceResponse(
contentType,
encoding,
response.status,
"OK",
headers,
cleanedHtml.byteInputStream()
)
} else {
WebResourceResponse(
contentType,
encoding,
response.status,
"OK",
headers,
inputStream
)
}
result = toWebResourceResponse(response)
} catch (e: Exception) {
LOG.error("Error processing HTTP response", e)
} finally {
@@ -141,14 +134,20 @@ object InternetHelperSingleton {
}
override fun onException(message: String?) {
LOG.error("Error during GET request: $message")
LOG.error("Request failed: $message")
result = null
latch.countDown()
}
})
// Write the body to the pipe, if any
if (pipeWrite != null) {
ParcelFileDescriptor.AutoCloseOutputStream(pipeWrite).use { outputStream ->
outputStream.write(body)
}
}
} catch (e: RemoteException) {
LOG.error("Error initiating GET request", e)
LOG.error("Error sending request to InternetHelper", e)
latch.countDown()
return null
}
@@ -156,4 +155,36 @@ object InternetHelperSingleton {
latch.await()
return result
}
}
private fun toWebResourceResponse(response: HttpResponse): WebResourceResponse {
val headers = response.headers.toMap()
val contentType = response.headers["content-type"]
?.substringBefore(";")
?.trim()
?: "application/octet-stream"
val encoding = response.headers["content-encoding"] ?: "UTF-8"
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
return if (contentType.equals("text/html", ignoreCase = true)) {
val rawHtml = inputStream.bufferedReader(Charset.forName(encoding)).use { it.readText() }
val cleanedHtml = Jsoup.parse(rawHtml).html()
WebResourceResponse(
contentType,
encoding,
response.status,
"OK",
headers,
cleanedHtml.byteInputStream()
)
} else {
WebResourceResponse(
contentType,
encoding,
response.status,
"OK",
headers,
inputStream
)
}
}
}
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util
import android.annotation.SuppressLint
import android.net.Uri
import android.webkit.WebResourceResponse
import nodomain.freeyourgadget.gadgetbridge.BuildConfig
@@ -70,18 +71,16 @@ class InternetUtils {
method: String = "GET",
requestHeaders: Map<String, String> = emptyMap(),
body: String? = null,
bodyContentType: String = "text/plain",
allowInsecure: Boolean = false
): String? {
val response: WebResourceResponse? = if (GBApplication.hasDirectInternetAccess()) {
directRequest(uri, method, requestHeaders, body, bodyContentType, allowInsecure)
directRequest(uri, method, requestHeaders, body, allowInsecure)
} else {
InternetHelperSingleton.send(
uri,
HttpRequest.Method.valueOf(method),
headersWithUserAgent(requestHeaders),
body,
bodyContentType,
body?.toByteArray(),
allowInsecure,
)
}
@@ -96,7 +95,6 @@ class InternetUtils {
method: String = "GET",
requestHeaders: Map<String, String> = emptyMap(),
body: String? = null,
bodyContentType: String = "application/json",
allowInsecure: Boolean = false
): JSONObject? {
val text = doStringRequest(
@@ -104,7 +102,6 @@ class InternetUtils {
method,
headersWithUserAgent(requestHeaders),
body,
bodyContentType,
allowInsecure
)
try {
@@ -127,7 +124,6 @@ class InternetUtils {
method = "GET",
requestHeaders = mapOf("User-Agent" to USER_AGENT),
body = null,
bodyContentType = "application/octet-stream",
allowInsecure = false
)
} else {
@@ -136,7 +132,6 @@ class InternetUtils {
HttpRequest.Method.GET,
requestHeaders = mapOf("User-Agent" to USER_AGENT),
null,
"application/octet-stream",
false
)
}
@@ -162,7 +157,6 @@ class InternetUtils {
method: String,
requestHeaders: Map<String, String>,
body: String?,
bodyContentType: String,
allowInsecure: Boolean
): WebResourceResponse {
@@ -178,7 +172,7 @@ class InternetUtils {
// Convert body string to RequestBody if allowed
val requestBody: RequestBody? =
if (body != null && method.uppercase() !in listOf("GET", "HEAD")) {
body.toRequestBody(bodyContentType.toMediaType())
body.toRequestBody("application/octet-stream".toMediaType())
} else null
// Configure HTTP method
@@ -238,8 +232,11 @@ class InternetUtils {
private fun createInsecureClient(): OkHttpClient {
return try {
val trustAllCerts = arrayOf<TrustManager>(
@SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
@@ -259,4 +256,4 @@ class InternetUtils {
}
}
}
}
}
@@ -39,6 +39,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
@@ -174,7 +175,7 @@ public class GBWebClient extends WebViewClient {
body = getFirstPostForUrl(requestedUri.toString());
LOG.debug("WEBVIEW POSTing with body: {}", body);
}
WebResourceResponse wrr = InternetHelperSingleton.INSTANCE.send(requestedUri, requestMethod, requestHeaders, body, "application/octet-stream", false);
WebResourceResponse wrr = InternetHelperSingleton.INSTANCE.send(requestedUri, requestMethod, requestHeaders, body != null ? body.getBytes(StandardCharsets.UTF_8) : null, false);
if (wrr != null && wrr.getStatusCode() < 400)
return wrr;
else
@@ -1,86 +0,0 @@
package nodomain.freeyourgadget.internethelper.aidl.http;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HttpHeaders implements Parcelable {
private final List<Pair<String, String>> headers = new ArrayList<>();
protected HttpHeaders(final Parcel in) {
final int numHeaders = in.readInt();
for (int i = 0; i < numHeaders; i++) {
final String k = in.readString();
final String v = in.readString();
headers.add(Pair.create(k, v));
}
}
public HttpHeaders(Map<String, String> headersMap) {
for (String key : headersMap.keySet()) {
headers.add(new Pair<>(key, headersMap.get(key)));
}
}
public HttpHeaders() {
}
public static final Creator<HttpHeaders> CREATOR = new Creator<HttpHeaders>() {
@Override
public HttpHeaders createFromParcel(final Parcel in) {
return new HttpHeaders(in);
}
@Override
public HttpHeaders[] newArray(final int size) {
return new HttpHeaders[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
dest.writeInt(headers.size());
for (final Pair<String, String> header : headers) {
dest.writeString(header.first);
dest.writeString(header.second);
}
}
public List<Pair<String, String>> getHeaders() {
return headers;
}
public void addHeader(final String key, final String value) {
headers.add(Pair.create(key, value));
}
public String get(final String key) {
for (final Pair<String, String> header : headers) {
if (header.first.equalsIgnoreCase(key)) {
return header.second;
}
}
return null;
}
public Map<String, String> toMap() {
final Map<String, String> ret = new HashMap<>();
for (final Pair<String, String> header : headers) {
ret.put(header.first, header.second);
}
return ret;
}
}
@@ -1,94 +0,0 @@
package nodomain.freeyourgadget.internethelper.aidl.http;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class HttpRequest implements Parcelable {
public enum Method {
GET,
POST,
HEAD,
PUT,
PATCH,
DELETE,
OPTIONS,
}
private final String url;
private final Method method;
private final String body;
private final String bodyContentType;
private final boolean allowInsecure;
private final HttpHeaders headers;
protected HttpRequest(final Parcel in) {
url = in.readString();
method = Method.values()[in.readInt()];
body = in.readString();
bodyContentType = in.readString();
allowInsecure = in.readByte() != 0; // readBoolean() requires API level 29
headers = in.readParcelable(HttpRequest.class.getClassLoader());
}
public HttpRequest(String url, Method method, String body, String bodyContentType, boolean allowInsecure, HttpHeaders headers) {
this.url = url;
this.method = method;
this.body = body;
this.bodyContentType = bodyContentType;
this.allowInsecure = allowInsecure;
this.headers = headers;
}
public static final Creator<HttpRequest> CREATOR = new Creator<>() {
@Override
public HttpRequest createFromParcel(final Parcel in) {
return new HttpRequest(in);
}
@Override
public HttpRequest[] newArray(final int size) {
return new HttpRequest[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
dest.writeString(url);
dest.writeInt(method.ordinal());
dest.writeString(body);
dest.writeString(bodyContentType);
dest.writeByte((byte) (allowInsecure ? 1 : 0)); // writeBoolean() requires API level 29
dest.writeParcelable(headers, 0);
}
public String getUrl() {
return url;
}
public Method getMethod() {
return method;
}
public String getBody() {
return body;
}
public String getBodyContentType() {
return bodyContentType;
}
public boolean getAllowInsecure() {
return allowInsecure;
}
public HttpHeaders getHeaders() {
return headers;
}
}
@@ -0,0 +1,25 @@
package nodomain.freeyourgadget.internethelper.aidl.http
import android.os.ParcelFileDescriptor
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class HttpRequest(
val url: String,
val method: Method,
val headers: Map<String, String> = emptyMap(),
val body: ParcelFileDescriptor? = null,
val allowInsecure: Boolean = false,
) : Parcelable {
@Suppress("unused") // they must be kept so they can be sent
enum class Method {
GET,
POST,
HEAD,
PUT,
PATCH,
DELETE,
OPTIONS,
}
}
@@ -1,61 +0,0 @@
package nodomain.freeyourgadget.internethelper.aidl.http;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class HttpResponse implements Parcelable {
private final int status;
private final HttpHeaders headers;
private final ParcelFileDescriptor body;
protected HttpResponse(final Parcel in) {
status = in.readInt();
headers = in.readParcelable(HttpResponse.class.getClassLoader());
body = in.readParcelable(HttpResponse.class.getClassLoader());
}
public HttpResponse(final int status, final HttpHeaders headers, final ParcelFileDescriptor body) {
this.status = status;
this.headers = headers;
this.body = body;
}
public static final Creator<HttpResponse> CREATOR = new Creator<HttpResponse>() {
@Override
public HttpResponse createFromParcel(final Parcel in) {
return new HttpResponse(in);
}
@Override
public HttpResponse[] newArray(final int size) {
return new HttpResponse[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
dest.writeInt(status);
dest.writeParcelable(headers, 0);
dest.writeParcelable(body, 0);
}
public int getStatus() {
return status;
}
public HttpHeaders getHeaders() {
return headers;
}
public ParcelFileDescriptor getBody() {
return body;
}
}
@@ -0,0 +1,12 @@
package nodomain.freeyourgadget.internethelper.aidl.http
import android.os.ParcelFileDescriptor
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class HttpResponse(
val status: Int,
val headers: Map<String, String> = emptyMap(),
val body: ParcelFileDescriptor? = null,
) : Parcelable