mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Centralize internet requests, removing Volley in the process
This commit is contained in:
committed by
Arjan Schrijver
parent
3b1eda31a6
commit
b0c0eda4f3
@@ -268,7 +268,6 @@ dependencies {
|
||||
implementation 'com.github.wax911.android-emojify:emojify:1.9.7'
|
||||
implementation 'com.github.wax911.android-emojify:gson:1.9.7'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.33.2'
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.2.1'
|
||||
implementation 'org.msgpack:msgpack-core:0.9.10'
|
||||
implementation 'com.github.ByteHamster:SearchPreference:2.7.3'
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
"path": "licenses/desugar_jdk_libs.txt"
|
||||
},
|
||||
{
|
||||
"name": "volley",
|
||||
"owner": "Google",
|
||||
"name": "okhttp",
|
||||
"owner": "Square, Inc.",
|
||||
"type": "Apache 2.0",
|
||||
"url": "https://github.com/google/volley",
|
||||
"url": "https://github.com/square/okhttp",
|
||||
"path": "licenses/apache-2.0.txt"
|
||||
},
|
||||
{
|
||||
|
||||
-4
@@ -58,10 +58,6 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
|
||||
|
||||
private Uri confUri;
|
||||
/**
|
||||
* When bgjs is enabled, this field refers to the WebViewSingleton,
|
||||
* otherwise it refers to the legacy webview from the activity_legacy_external_pebble_js layout
|
||||
*/
|
||||
private WebView myWebView;
|
||||
|
||||
@Override
|
||||
|
||||
+13
-154
@@ -23,41 +23,24 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient
|
||||
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback
|
||||
import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class RebbleAppStoreActivity : AbstractGBActivity() {
|
||||
val LOG: Logger = LoggerFactory.getLogger(RebbleAppStoreActivity::class.java)
|
||||
@@ -99,148 +82,24 @@ class RebbleAppStoreActivity : AbstractGBActivity() {
|
||||
}
|
||||
|
||||
private fun downloadInstallWatchapp(url: Uri) {
|
||||
if (GBApplication.hasDirectInternetAccess()) {
|
||||
downloadBinaryFile(url) { file ->
|
||||
installFile(file)
|
||||
}
|
||||
} else {
|
||||
val httpHeaders = HttpHeaders()
|
||||
val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders)
|
||||
InternetHelperSingleton.getHttpService()
|
||||
?.get(httpGetRequest, object : IHttpCallback.Stub() {
|
||||
override fun onResponse(response: HttpResponse) {
|
||||
val contentType = response.headers["content-type"]?.split(";")?.get(0)
|
||||
if (!contentType.equals("application/octet-stream") && !contentType.equals("application/zip")) {
|
||||
GB.toast(
|
||||
getString(
|
||||
R.string.rebble_appstore_download_failed_wrong_content_type,
|
||||
contentType
|
||||
),
|
||||
Toast.LENGTH_LONG, GB.ERROR
|
||||
)
|
||||
LOG.error("Received content-type $contentType but expected application/octet-stream or application/zip")
|
||||
return
|
||||
}
|
||||
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
|
||||
val filename = url.lastPathSegment ?: "downloaded_file.pbw"
|
||||
val cacheDir = applicationContext.externalCacheDir ?: return
|
||||
val cacheFile = File(cacheDir, filename)
|
||||
val outputStream: OutputStream = FileOutputStream(cacheFile)
|
||||
val buffer = ByteArray(1024)
|
||||
var len: Int
|
||||
while (inputStream.read(buffer).also { len = it } > 0) {
|
||||
outputStream.write(buffer, 0, len)
|
||||
}
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
installFile(cacheFile)
|
||||
}
|
||||
val filename = url.lastPathSegment ?: "downloaded_file.bin"
|
||||
val cacheDir = applicationContext.externalCacheDir
|
||||
val targetFile = File(cacheDir, filename)
|
||||
|
||||
override fun onException(message: String?) {
|
||||
GB.toast(
|
||||
getString(R.string.rebble_appstore_download_failed, message),
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR
|
||||
)
|
||||
LOG.error("Failed downloading file: $message")
|
||||
}
|
||||
})
|
||||
InternetUtils.downloadBinaryFile(url, targetFile) { file ->
|
||||
installFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadInstallWatchappById(storeId: String) {
|
||||
val appUrl = "https://appstore-api.rebble.io/api/v1/apps/id/$storeId"
|
||||
if (GBApplication.hasDirectInternetAccess()) {
|
||||
val requestQueue = Volley.newRequestQueue(this)
|
||||
val jsonObjectRequest = JsonObjectRequest(
|
||||
Request.Method.GET, appUrl, null,
|
||||
{ response ->
|
||||
val dataArray = response.getJSONArray("data")
|
||||
val firstAppObject = dataArray.getJSONObject(0)
|
||||
val latestRelease = firstAppObject.getJSONObject("latest_release")
|
||||
val pbwFile = latestRelease.getString("pbw_file")
|
||||
downloadInstallWatchapp(pbwFile.toUri())
|
||||
},
|
||||
{ error ->
|
||||
GB.toast(
|
||||
getString(
|
||||
R.string.rebble_appstore_fetching_download_file_failed,
|
||||
error
|
||||
), Toast.LENGTH_LONG, GB.ERROR
|
||||
)
|
||||
LOG.error("Failed fetching download file URL", error)
|
||||
}
|
||||
)
|
||||
requestQueue.add(jsonObjectRequest)
|
||||
} else {
|
||||
val httpHeaders = HttpHeaders()
|
||||
val httpGetRequest = HttpGetRequest(appUrl, httpHeaders)
|
||||
InternetHelperSingleton.getHttpService()
|
||||
?.get(httpGetRequest, object : IHttpCallback.Stub() {
|
||||
override fun onResponse(response: HttpResponse) {
|
||||
val contentType = response.headers["content-type"]?.split(";")?.get(0)
|
||||
if (!contentType.equals("application/json")) {
|
||||
GB.toast(
|
||||
getString(
|
||||
R.string.rebble_appstore_fetch_app_info_failed_content_type,
|
||||
contentType
|
||||
),
|
||||
Toast.LENGTH_LONG, GB.ERROR
|
||||
)
|
||||
LOG.error("Received content-type $contentType but expected application/json")
|
||||
return
|
||||
}
|
||||
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
|
||||
val responseBody =
|
||||
InputStreamReader(inputStream, StandardCharsets.UTF_8).readText()
|
||||
val jsonObject = JSONObject(responseBody)
|
||||
val dataArray = jsonObject.getJSONArray("data")
|
||||
val firstAppObject = dataArray.getJSONObject(0)
|
||||
val latestRelease = firstAppObject.getJSONObject("latest_release")
|
||||
val pbwFile = latestRelease.getString("pbw_file")
|
||||
downloadInstallWatchapp(pbwFile.toUri())
|
||||
}
|
||||
|
||||
override fun onException(message: String?) {
|
||||
GB.toast(
|
||||
getString(
|
||||
R.string.rebble_appstore_fetching_download_file_failed,
|
||||
message
|
||||
), Toast.LENGTH_LONG, GB.ERROR
|
||||
)
|
||||
LOG.error("Error downloading file: $message")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadBinaryFile(url: Uri, onComplete: (File) -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val client = OkHttpClient()
|
||||
val request = okhttp3.Request.Builder().url(url.toString()).build()
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
LOG.error("Downloading $url failed: ${response.code}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val filename = url.lastPathSegment ?: "downloaded_file.pbw"
|
||||
val cacheDir = applicationContext.externalCacheDir
|
||||
val cacheFile = File(cacheDir, filename)
|
||||
val outputStream = FileOutputStream(cacheFile)
|
||||
|
||||
val inputStream = response.body.byteStream()
|
||||
inputStream.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
|
||||
onComplete(cacheFile)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Downloading $url failed: ", e)
|
||||
}
|
||||
val response: JSONObject? = InternetUtils.doRequest(appUrl.toUri())
|
||||
if (response != null) {
|
||||
val dataArray = response.getJSONArray("data")
|
||||
val firstAppObject = dataArray.getJSONObject(0)
|
||||
val latestRelease = firstAppObject.getJSONObject("latest_release")
|
||||
val pbwFile = latestRelease.getString("pbw_file")
|
||||
downloadInstallWatchapp(pbwFile.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-11
@@ -32,11 +32,8 @@ import android.webkit.DownloadListener;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.PermissionRequest;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@@ -55,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
|
||||
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
|
||||
|
||||
public class AppsManagementActivity extends AbstractGBActivity {
|
||||
@@ -195,7 +193,6 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
private void initViews() {
|
||||
//https://stackoverflow.com/questions/4325639/android-calling-javascript-functions-in-webview
|
||||
webView = findViewById(R.id.webview);
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDatabaseEnabled(true);
|
||||
@@ -212,12 +209,6 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
public void onPageFinished(WebView view, String weburl){
|
||||
//webView.loadUrl("javascript:showToast('WebView in Espruino')");
|
||||
}
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView vw, WebResourceRequest request) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
|
||||
vw.getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||
@@ -237,7 +228,7 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
}, 1000);
|
||||
|
||||
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
webView.setWebChromeClient(new GBChromeClient() {
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
request.grant(request.getResources());
|
||||
|
||||
-1
@@ -55,7 +55,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton;
|
||||
|
||||
public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
|
||||
+12
-147
@@ -19,6 +19,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP_SIZE;
|
||||
@@ -58,21 +59,9 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.DefaultRetryPolicy;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.RequestQueue;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.StringRequest;
|
||||
import com.android.volley.toolbox.Volley;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -151,11 +140,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.VolleyUtils;
|
||||
|
||||
public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BangleJSDeviceSupport.class);
|
||||
@@ -189,10 +177,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
// this stores the globalUartReceiver (for uart.tx intents)
|
||||
private BroadcastReceiver globalUartReceiver = null;
|
||||
|
||||
// used to make HTTP requests and handle responses
|
||||
private RequestQueue requestQueue = null;
|
||||
private RequestQueue insecureRequestQueue = null;
|
||||
|
||||
/// Maximum amount of characters to store in receiveHistory
|
||||
public static final int MAX_RECEIVE_HISTORY_CHARS = 100000;
|
||||
/// Used to avoid spamming logs with ACTION_DEVICE_CHANGED messages
|
||||
@@ -226,7 +210,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
super.dispose();
|
||||
stopGlobalUartReceiver();
|
||||
stopLocationUpdate();
|
||||
stopRequestQueue();
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
@@ -246,38 +229,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
gpsUpdateSetup = false;
|
||||
}
|
||||
|
||||
private void stopRequestQueue() {
|
||||
if (requestQueue != null) {
|
||||
requestQueue.stop();
|
||||
}
|
||||
if (insecureRequestQueue != null) {
|
||||
insecureRequestQueue.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private RequestQueue getRequestQueue(final boolean insecure) {
|
||||
if (insecure) {
|
||||
if (insecureRequestQueue == null) {
|
||||
try {
|
||||
insecureRequestQueue = Volley.newRequestQueue(
|
||||
getContext(),
|
||||
VolleyUtils.createInsecureHurlStack()
|
||||
);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to initialized insecure request queue", e);
|
||||
// fallback to secure one
|
||||
return getRequestQueue(false);
|
||||
}
|
||||
}
|
||||
return insecureRequestQueue;
|
||||
} else {
|
||||
if (requestQueue == null) {
|
||||
requestQueue = Volley.newRequestQueue(getContext());
|
||||
}
|
||||
return requestQueue;
|
||||
}
|
||||
}
|
||||
|
||||
private void addReceiveHistory(String s) {
|
||||
receiveHistory += s;
|
||||
if (receiveHistory.length() > MAX_RECEIVE_HISTORY_CHARS)
|
||||
@@ -924,123 +875,37 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
||||
}
|
||||
|
||||
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
if (! devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) {
|
||||
if (!devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) {
|
||||
uartTxJSONError("http", "Internet access not enabled for this watch", id);
|
||||
return;
|
||||
}
|
||||
|
||||
String url = json.getString("url");
|
||||
final boolean insecure = json.optBoolean("insecure", false);
|
||||
int method = Request.Method.GET;
|
||||
if (json.has("method")) {
|
||||
String m = json.getString("method").toLowerCase(Locale.US);
|
||||
if (m.equals("get")) method = Request.Method.GET;
|
||||
else if (m.equals("post")) method = Request.Method.POST;
|
||||
else if (m.equals("head")) method = Request.Method.HEAD;
|
||||
else if (m.equals("put")) method = Request.Method.PUT;
|
||||
else if (m.equals("patch")) method = Request.Method.PATCH;
|
||||
else if (m.equals("delete")) method = Request.Method.DELETE;
|
||||
else uartTxJSONError("http", "Unknown HTTP method "+m,id);
|
||||
}
|
||||
String method = json.getString("method").toUpperCase(Locale.US);
|
||||
|
||||
byte[] _body = null;
|
||||
String body = null;
|
||||
if (json.has("body"))
|
||||
_body = json.getString("body").getBytes();
|
||||
final byte[] body = _body;
|
||||
body = json.getString("body");
|
||||
|
||||
Map<String,String> _headers = null;
|
||||
Map<String,String> headers = null;
|
||||
if (json.has("headers")) {
|
||||
JSONObject h = json.getJSONObject("headers");
|
||||
_headers = new HashMap<String,String>();
|
||||
headers = new HashMap<String,String>();
|
||||
Iterator<String> iter = h.keys();
|
||||
while (iter.hasNext()) {
|
||||
String key = iter.next();
|
||||
try {
|
||||
String value = h.getString(key);
|
||||
_headers.put(key, value);
|
||||
headers.put(key, value);
|
||||
} catch (JSONException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
final Map<String,String> headers = _headers;
|
||||
if (headers == null) headers = emptyMap();
|
||||
|
||||
String _xmlPath = "";
|
||||
String _xmlReturn = "";
|
||||
try {
|
||||
_xmlPath = json.getString("xpath");
|
||||
_xmlReturn = json.getString("return");
|
||||
} catch (JSONException e) {
|
||||
}
|
||||
final String xmlPath = _xmlPath;
|
||||
final String xmlReturn = _xmlReturn;
|
||||
// Request a string response from the provided URL.
|
||||
StringRequest stringRequest = new StringRequest(method, url,
|
||||
new Response.Listener<String>() {
|
||||
@Override
|
||||
public void onResponse(String response) {
|
||||
JSONObject o = new JSONObject();
|
||||
if (xmlPath.length() != 0) {
|
||||
try {
|
||||
Document doc = Jsoup.parse(response);
|
||||
Elements result = doc.selectXpath(xmlPath);
|
||||
if (xmlReturn.equals("array")) {
|
||||
response = null; // don't add it below
|
||||
JSONArray arr = new JSONArray();
|
||||
for (int i = 0; i < result.size(); i++)
|
||||
arr.put(result.get(i).text());
|
||||
o.put("resp", arr);
|
||||
} else { // else return only first!
|
||||
response = "";
|
||||
if (!result.isEmpty())
|
||||
response = result.get(0).text();
|
||||
}
|
||||
} catch (Exception error) {
|
||||
uartTxJSONError("http", error.toString(), id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
o.put("t", "http");
|
||||
if( id!=null)
|
||||
o.put("id", id);
|
||||
if (response!=null)
|
||||
o.put("resp", response);
|
||||
} catch (JSONException e) {
|
||||
GB.toast(getContext(), "HTTP: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
uartTxJSON("http", o);
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
uartTxJSONError("http", error.toString(), id);
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public byte[] getBody() throws AuthFailureError {
|
||||
if (body == null) return super.getBody();
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
// clone the data from super.getHeaders() so we can write to it
|
||||
Map<String, String> h = new HashMap<>(super.getHeaders());
|
||||
if (headers != null) {
|
||||
for (String key : headers.keySet()) {
|
||||
String value = headers.get(key);
|
||||
h.put(key, value);
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
};
|
||||
if (json.has("timeout")) {
|
||||
int timeout = json.getInt("timeout");
|
||||
stringRequest.setRetryPolicy(new DefaultRetryPolicy(timeout, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
|
||||
}
|
||||
RequestQueue queue = getRequestQueue(insecure);
|
||||
queue.add(stringRequest);
|
||||
JSONObject o = InternetUtils.Companion.doRequest(Uri.parse(url), method, headers, body, "application/json", insecure);
|
||||
uartTxJSON("http", o);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+48
-51
@@ -43,12 +43,6 @@ object InternetHelperSingleton {
|
||||
private var internetHelperBound = false
|
||||
private var internetHelper: IHttpService? = null
|
||||
|
||||
fun getHttpService(): IHttpService? {
|
||||
ensureInternetHelperBound()
|
||||
return internetHelper
|
||||
}
|
||||
|
||||
//Internet helper outgoing connection
|
||||
private val internetHelperConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName?, service: IBinder?) {
|
||||
LOG.info("internet helper service bound")
|
||||
@@ -63,8 +57,9 @@ object InternetHelperSingleton {
|
||||
}
|
||||
}
|
||||
|
||||
fun isInternetHelperBound(): Boolean {
|
||||
return internetHelperBound
|
||||
fun getHttpService(): IHttpService? {
|
||||
ensureInternetHelperBound()
|
||||
return internetHelper
|
||||
}
|
||||
|
||||
fun ensureInternetHelperBound(): Boolean {
|
||||
@@ -92,64 +87,66 @@ object InternetHelperSingleton {
|
||||
|
||||
@Throws(RemoteException::class, InterruptedException::class)
|
||||
fun send(webRequest: Uri): WebResourceResponse? {
|
||||
val httpHeaders = HttpHeaders()
|
||||
val httpGetRequest = HttpGetRequest(webRequest.toString(), httpHeaders)
|
||||
val latch = CountDownLatch(1)
|
||||
val internetResponseCapsule = Capsule<WebResourceResponse?>()
|
||||
var result: WebResourceResponse? = null
|
||||
val request = HttpGetRequest(webRequest.toString(), HttpHeaders())
|
||||
|
||||
LOG.debug("Forwarding GET request to {} to internet helper app", webRequest)
|
||||
try {
|
||||
internetHelper?.get(httpGetRequest, object : IHttpCallback.Stub() {
|
||||
@Throws(RemoteException::class)
|
||||
internetHelper?.get(request, object : IHttpCallback.Stub() {
|
||||
override fun onResponse(response: HttpResponse) {
|
||||
// Extract headers
|
||||
val contentType = response.headers["content-type"]?.split(";")?.get(0) ?: "text/html"
|
||||
val contentEncoding = response.headers["content-encoding"] ?: "UTF-8"
|
||||
try {
|
||||
val headers = response.headers.toMap()
|
||||
val contentType = response.headers["content-type"]
|
||||
?.substringBefore(";")
|
||||
?.trim()
|
||||
?: "text/html"
|
||||
val encoding = response.headers["content-encoding"] ?: "UTF-8"
|
||||
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
|
||||
|
||||
// Retrieve original payload as InputStream
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up and fix received malformed HTML
|
||||
if (contentType.equals("text/html", ignoreCase = true)) {
|
||||
val rawHtml = inputStream.bufferedReader(Charset.forName(contentEncoding)).use { it.readText() }
|
||||
val cleanedHtml = Jsoup.parse(rawHtml).html()
|
||||
val internetResponse = WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.status,
|
||||
"OK",
|
||||
response.headers.toMap(),
|
||||
cleanedHtml.byteInputStream()
|
||||
)
|
||||
internetResponseCapsule.set(internetResponse)
|
||||
} else {
|
||||
// If not text/html, return original response
|
||||
val internetResponse = WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.status,
|
||||
"OK",
|
||||
response.headers.toMap(),
|
||||
inputStream
|
||||
)
|
||||
internetResponseCapsule.set(internetResponse)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error processing HTTP response", e)
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun onException(message: String?) {
|
||||
LOG.error("Error during GET request: $message")
|
||||
result = null
|
||||
latch.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
} catch (e: RemoteException) {
|
||||
LOG.error("Error during GET request", e)
|
||||
}
|
||||
try {
|
||||
latch.await()
|
||||
} catch (e: InterruptedException) {
|
||||
LOG.error("Error during GET request", e)
|
||||
LOG.error("Error initiating GET request", e)
|
||||
latch.countDown()
|
||||
return null
|
||||
}
|
||||
|
||||
return internetResponseCapsule.get()
|
||||
latch.await()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/* Copyright (C) 2025 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.WebResourceResponse
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class InternetUtils {
|
||||
|
||||
companion object {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(InternetUtils::class.java)
|
||||
private val defaultClient = OkHttpClient()
|
||||
|
||||
/**
|
||||
* Performs an HTTP request to the given URI, optionally allowing insecure connections.
|
||||
*/
|
||||
fun doRequest(
|
||||
uri: Uri,
|
||||
method: String = "GET",
|
||||
requestHeaders: Map<String, String> = emptyMap(),
|
||||
body: String? = null,
|
||||
bodyContentType: String = "text/plain",
|
||||
insecure: Boolean = false
|
||||
): JSONObject? {
|
||||
val response: WebResourceResponse? = if (GBApplication.hasDirectInternetAccess()) {
|
||||
directRequest(uri, method, requestHeaders, body, bodyContentType, insecure)
|
||||
} else {
|
||||
InternetHelperSingleton.send(uri)
|
||||
}
|
||||
if (response == null) return null
|
||||
|
||||
// Convert response InputStream to String
|
||||
val text = response.data.bufferedReader().use { it.readText() }
|
||||
return JSONObject(text)
|
||||
}
|
||||
|
||||
fun downloadBinaryFile(
|
||||
uri: Uri,
|
||||
targetFile: File,
|
||||
onComplete: (File) -> Unit
|
||||
) {
|
||||
try {
|
||||
val response: WebResourceResponse? = if (GBApplication.hasDirectInternetAccess()) {
|
||||
directRequest(
|
||||
uri = uri,
|
||||
method = "GET",
|
||||
requestHeaders = emptyMap(),
|
||||
body = null,
|
||||
bodyContentType = "application/octet-stream",
|
||||
insecure = false
|
||||
)
|
||||
} else {
|
||||
InternetHelperSingleton.send(uri)
|
||||
}
|
||||
|
||||
response?.data?.use { input ->
|
||||
FileOutputStream(targetFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
onComplete(targetFile)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Downloading $uri failed: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct HTTP request using OkHttp.
|
||||
*/
|
||||
private fun directRequest(
|
||||
uri: Uri,
|
||||
method: String,
|
||||
requestHeaders: Map<String, String>,
|
||||
body: String?,
|
||||
bodyContentType: String,
|
||||
insecure: Boolean
|
||||
): WebResourceResponse {
|
||||
|
||||
val client = if (insecure) createInsecureClient() else defaultClient
|
||||
|
||||
val builder = Request.Builder().url(uri.toString())
|
||||
|
||||
// Apply request headers
|
||||
for ((key, value) in requestHeaders) {
|
||||
builder.addHeader(key, value)
|
||||
}
|
||||
|
||||
// Convert body string to RequestBody if allowed
|
||||
val requestBody: RequestBody? =
|
||||
if (body != null && method.uppercase() !in listOf("GET", "HEAD")) {
|
||||
body.toRequestBody(bodyContentType.toMediaType())
|
||||
} else null
|
||||
|
||||
// Configure HTTP method
|
||||
when (method.uppercase()) {
|
||||
"GET" -> builder.get()
|
||||
"HEAD" -> builder.head()
|
||||
"DELETE" -> if (requestBody != null) builder.delete(requestBody) else builder.delete()
|
||||
else -> builder.method(method.uppercase(), requestBody)
|
||||
}
|
||||
|
||||
// Execute request
|
||||
client.newCall(builder.build()).execute().use { response ->
|
||||
val statusCode = response.code
|
||||
val message = if (!response.message.isEmpty()) response.message else "OK"
|
||||
val headers = response.headers.toMap()
|
||||
|
||||
val contentType = response.header("content-type") ?: "application/octet-stream"
|
||||
val encoding = response.header("content-encoding") ?: "UTF-8"
|
||||
|
||||
// HEAD: empty body
|
||||
if (method.equals("HEAD", true)) {
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
encoding,
|
||||
statusCode,
|
||||
message,
|
||||
headers,
|
||||
ByteArrayInputStream(ByteArray(0))
|
||||
)
|
||||
}
|
||||
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
encoding,
|
||||
statusCode,
|
||||
message,
|
||||
headers,
|
||||
ByteArrayInputStream(response.body.bytes())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OkHttp Headers to a Map suitable for WebResourceResponse.
|
||||
*/
|
||||
private fun Headers.toMap(): Map<String, String> {
|
||||
val result = LinkedHashMap<String, String>()
|
||||
for (name in this.names()) {
|
||||
result[name] = this.values(name).joinToString(",")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OkHttpClient that accepts all SSL certificates (insecure).
|
||||
*/
|
||||
private fun createInsecureClient(): OkHttpClient {
|
||||
return try {
|
||||
val trustAllCerts = arrayOf<TrustManager>(
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
|
||||
}
|
||||
)
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, trustAllCerts, SecureRandom())
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||
.hostnameVerifier { _, _ -> true }
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to create insecure OkHttp client", e)
|
||||
defaultClient
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.android.volley.toolbox.HurlStack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class VolleyUtils {
|
||||
public static HurlStack createInsecureHurlStack() throws Exception {
|
||||
// Trust all certificates
|
||||
final TrustManager[] trustAllCerts = new TrustManager[]{new InsecureTrustManager()};
|
||||
final SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustAllCerts, new SecureRandom());
|
||||
|
||||
return new HurlStack(null, sslContext.getSocketFactory()) {
|
||||
@Override
|
||||
protected HttpURLConnection createConnection(final URL url) throws IOException {
|
||||
final HttpURLConnection httpURLConnection = super.createConnection(url);
|
||||
if (httpURLConnection instanceof HttpsURLConnection httpsURLConnection) {
|
||||
// Trust all hostnames
|
||||
httpsURLConnection.setHostnameVerifier((hostname, session) -> true);
|
||||
}
|
||||
return httpURLConnection;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
public static class InsecureTrustManager implements X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Override
|
||||
public void checkClientTrusted(final X509Certificate[] chain, final String authType) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Override
|
||||
public void checkServerTrusted(final X509Certificate[] chain, final String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[]{};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,28 +18,27 @@ package nodomain.freeyourgadget.gadgetbridge.webview;
|
||||
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class GBChromeClient extends WebChromeClient {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GBChromeClient.class);
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||
if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
|
||||
GB.toast(formatConsoleMessage(consoleMessage), Toast.LENGTH_LONG, GB.ERROR);
|
||||
//TODO: show error page
|
||||
LOG.error(formatConsoleMessage(consoleMessage));
|
||||
//TODO: show small error indication to user, allowing them to view the error(s)
|
||||
}
|
||||
return super.onConsoleMessage(consoleMessage);
|
||||
}
|
||||
|
||||
private static String formatConsoleMessage(ConsoleMessage message) {
|
||||
String sourceId = message.sourceId();
|
||||
if (sourceId == null || sourceId.length() == 0) {
|
||||
if (sourceId == null || sourceId.isEmpty()) {
|
||||
sourceId = "unknown";
|
||||
}
|
||||
return String.format("%s (at %s: %d)", message.message(), sourceId, message.lineNumber());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ public class GBWebClient extends WebViewClient {
|
||||
Uri parsedUri = Uri.parse(url);
|
||||
|
||||
if (parsedUri.getScheme().startsWith("http")) {
|
||||
if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) {
|
||||
if (GBApplication.hasInternetAccess()) {
|
||||
view.loadUrl(url);
|
||||
} else {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
|
||||
Reference in New Issue
Block a user