mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Initial support for using the Gadgetbridge-internethelper add-on app
Imported rebased code from the jr-internet-helper branch # Conflicts: # app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java # app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java
This commit is contained in:
committed by
Arjan Schrijver
parent
638f9e52fd
commit
38d877556d
@@ -51,6 +51,9 @@
|
||||
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
|
||||
<uses-permission android:name="me.hackerchick.catima.debug.READ_CARDS"/>
|
||||
|
||||
<!-- Internet Helper -->
|
||||
<uses-permission android:name="nodomain.freeyourgadget.internethelper.INTERNET"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.ftp;
|
||||
|
||||
parcelable FtpEntry;
|
||||
@@ -0,0 +1,11 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.ftp;
|
||||
|
||||
import nodomain.freeyourgadget.internethelper.aidl.ftp.FtpEntry;
|
||||
|
||||
oneway interface IFtpCallback {
|
||||
void onConnect(boolean success, String msg);
|
||||
void onLogin(boolean success, String msg);
|
||||
void onList(String path, in List<FtpEntry> entries);
|
||||
void onUpload(String path, boolean success, String msg);
|
||||
void onDownload(String path, boolean success, String msg);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.ftp;
|
||||
|
||||
import nodomain.freeyourgadget.internethelper.aidl.ftp.IFtpCallback;
|
||||
|
||||
interface IFtpService {
|
||||
int version();
|
||||
|
||||
String createClient(IFtpCallback callback);
|
||||
void destroyClient(String client);
|
||||
|
||||
void connect(String client, String host, int port);
|
||||
void disconnect(String client);
|
||||
|
||||
void login(String client, String username, String password);
|
||||
|
||||
void list(String client, String path);
|
||||
void upload(String client, String localSrc, String remoteDest);
|
||||
void download(String client, String remoteSrc, String localDest);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
parcelable HttpGetRequest;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
parcelable HttpPostRequest;
|
||||
@@ -0,0 +1,3 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
parcelable HttpResponse;
|
||||
@@ -0,0 +1,8 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse;
|
||||
|
||||
oneway interface IHttpCallback {
|
||||
void onResponse(in HttpResponse response);
|
||||
void onException(in String message);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest;
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpPostRequest;
|
||||
import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback;
|
||||
|
||||
interface IHttpService {
|
||||
int version();
|
||||
|
||||
void get(in HttpGetRequest request, IHttpCallback cb);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.wifi;
|
||||
|
||||
oneway interface IWifiCallback {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.wifi;
|
||||
|
||||
import nodomain.freeyourgadget.internethelper.aidl.wifi.IWifiCallback;
|
||||
|
||||
interface IWifiService {
|
||||
int version();
|
||||
String getCurrentSsid();
|
||||
void connect(String ssid, String password, IWifiCallback cb);
|
||||
}
|
||||
+5
-4
@@ -17,6 +17,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -56,8 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
|
||||
|
||||
public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
|
||||
@@ -188,9 +188,10 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
//needed for localstorage
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
// #3373 #3424 - Fix configuration for pebble apps
|
||||
// TODO: this should use a WebViewAssetLoader
|
||||
//allow local js files access
|
||||
webSettings.setAllowContentAccess(true);
|
||||
webSettings.setAllowFileAccess(true);
|
||||
webSettings.setAllowFileAccessFromFileURLs(true);
|
||||
|
||||
JSInterface gbJSInterface = new JSInterface(device, uuid);
|
||||
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
|
||||
|
||||
+98
-4
@@ -16,13 +16,22 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_WEBVIEW_URL;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Base64;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.JavascriptInterface;
|
||||
@@ -30,6 +39,7 @@ import android.webkit.PermissionRequest;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
@@ -43,6 +53,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@@ -50,9 +62,14 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
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.Capsule;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_WEBVIEW_URL;
|
||||
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 nodomain.freeyourgadget.internethelper.aidl.http.IHttpService;
|
||||
|
||||
public class AppsManagementActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppsManagementActivity.class);
|
||||
@@ -79,6 +96,15 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_banglejs_apps_management);
|
||||
|
||||
final Intent intent1 = new Intent("nodomain.freeyourgadget.internethelper.HttpService");
|
||||
intent1.setPackage("nodomain.freeyourgadget.internethelper");
|
||||
boolean res = getApplicationContext().bindService(intent1, mHttpConnection, Context.BIND_AUTO_CREATE);
|
||||
if (res) {
|
||||
LOG.info("Bound to HttpService");
|
||||
} else {
|
||||
LOG.warn("Could not bind to HttpService");
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
Bundle bundle = intent.getExtras();
|
||||
if (bundle != null) {
|
||||
@@ -193,6 +219,21 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
// see onActivityResult
|
||||
}
|
||||
|
||||
private volatile IHttpService iHttpService;
|
||||
private final CountDownLatch latchInit = new CountDownLatch(1);
|
||||
|
||||
private final ServiceConnection mHttpConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(final ComponentName className, final IBinder service) {
|
||||
LOG.info("onServiceConnected: {}", className);
|
||||
iHttpService = IHttpService.Stub.asInterface(service);
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(final ComponentName className) {
|
||||
LOG.error("Service has unexpectedly disconnected: {}", className);
|
||||
iHttpService = null;
|
||||
}
|
||||
};
|
||||
|
||||
private void initViews() {
|
||||
//https://stackoverflow.com/questions/4325639/android-calling-javascript-functions-in-webview
|
||||
webView = findViewById(R.id.webview);
|
||||
@@ -209,9 +250,7 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
webView.setWebContentsDebuggingEnabled(true); // FIXME
|
||||
|
||||
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()));
|
||||
String url = devicePrefs.getString(PREF_BANGLEJS_WEBVIEW_URL, "").trim();
|
||||
if (url.isEmpty()) url = "https://banglejs.com/apps/android.html";
|
||||
webView.loadUrl(url);
|
||||
final String url = devicePrefs.getString(PREF_BANGLEJS_WEBVIEW_URL, "https://banglejs.com/apps/android.html").trim();
|
||||
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
@Override
|
||||
@@ -225,12 +264,67 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
LOG.info("shouldIntercept {} {} {}", request.getMethod(), request.getUrl(), iHttpService != null);
|
||||
if (!request.getMethod().equalsIgnoreCase("get")) {
|
||||
return super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
if (iHttpService == null) {
|
||||
return super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) {
|
||||
httpHeaders.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
final HttpGetRequest httpGetRequest = new HttpGetRequest(request.getUrl().toString(), httpHeaders);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
final Capsule<WebResourceResponse> internetResponseCapsule = new Capsule<>();
|
||||
try {
|
||||
iHttpService.get(httpGetRequest, new IHttpCallback.Stub() {
|
||||
@Override
|
||||
public void onResponse(HttpResponse response) throws RemoteException {
|
||||
WebResourceResponse internetResponse = new WebResourceResponse(
|
||||
response.getHeaders().get("content-type"),
|
||||
response.getHeaders().get("content-encoding"),
|
||||
response.getStatus(), "OK",
|
||||
response.getHeaders().toMap(),
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(response.getBody())
|
||||
);
|
||||
internetResponseCapsule.set(internetResponse);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(String message) throws RemoteException {
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return internetResponseCapsule.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||
Toast.makeText(AppsManagementActivity.this, "Error:" + description, Toast.LENGTH_SHORT).show();
|
||||
view.loadUrl("about:blank");
|
||||
}
|
||||
});
|
||||
|
||||
final Looper mainLooper = Looper.getMainLooper();
|
||||
new Handler(mainLooper).postDelayed(() -> {
|
||||
webView.loadUrl(url);
|
||||
}, 1000);
|
||||
|
||||
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
|
||||
+2
-2
@@ -168,12 +168,12 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(final GBDevice device) {
|
||||
return BuildConfig.INTERNET_ACCESS;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity(final GBDevice device) {
|
||||
return BuildConfig.INTERNET_ACCESS ? AppsManagementActivity.class : null;
|
||||
return AppsManagementActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* Copyright (C) 2023 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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.network;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class InternetHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InternetHelper.class);
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public InternetHelper(final Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return "nodomain.freeyourgadget.internethelper";
|
||||
}
|
||||
|
||||
public String getPermission() {
|
||||
return getPackageName() + ".INTERNET";
|
||||
}
|
||||
|
||||
public boolean requestPermissions(final Activity activity) {
|
||||
if (ActivityCompat.checkSelfPermission(mContext, getPermission()) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.warn("No permission to access internet, requesting");
|
||||
ActivityCompat.requestPermissions(activity, new String[]{getPermission()}, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -19,8 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
@@ -45,9 +43,9 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherMapper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
|
||||
|
||||
public class GBWebClient extends WebViewClient {
|
||||
@@ -84,12 +82,8 @@ public class GBWebClient extends WebViewClient {
|
||||
if (requestedUri.getHost() != null && (StringUtils.indexOfAny(requestedUri.getHost(), AllowedDomains) != -1)) {
|
||||
if (WebViewSingleton.getInstance().internetHelperBound) {
|
||||
LOG.debug("WEBVIEW forwarding request to the internet helper");
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("URL", requestedUri.toString());
|
||||
Message webRequest = Message.obtain();
|
||||
webRequest.setData(bundle);
|
||||
try {
|
||||
return WebViewSingleton.getInstance().send(webRequest);
|
||||
return WebViewSingleton.getInstance().send(requestedUri);
|
||||
} catch (RemoteException | InterruptedException e) {
|
||||
LOG.warn("Error downloading data from " + requestedUri, e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/* Copyright (C) 2023 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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
public final class Capsule<T> {
|
||||
private T value;
|
||||
|
||||
public Capsule() {
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
public Capsule(final T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void set(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -44,9 +43,12 @@ import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
@@ -462,4 +464,27 @@ public class FileUtils {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String md5sum(final InputStream is) {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("MD5");
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
GB.log("Failed to get MD5 digest instance", GB.ERROR, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
return String.format("%1$32s", GB.hexdump(digest.digest())).replace(' ', '0').toLowerCase(Locale.ROOT);
|
||||
} catch (final IOException e) {
|
||||
GB.log("Error reading file", GB.ERROR, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,27 +24,21 @@ import android.content.Intent;
|
||||
import android.content.MutableContextWrapper;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@@ -52,6 +46,11 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
|
||||
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 nodomain.freeyourgadget.internethelper.aidl.http.IHttpService;
|
||||
|
||||
public class WebViewSingleton {
|
||||
|
||||
@@ -62,12 +61,10 @@ public class WebViewSingleton {
|
||||
private MutableContextWrapper contextWrapper;
|
||||
private Looper mainLooper;
|
||||
private UUID currentRunningUUID;
|
||||
private Messenger internetHelper = null;
|
||||
private IHttpService internetHelper = null;
|
||||
public boolean internetHelperBound;
|
||||
private boolean internetHelperInstalled;
|
||||
private CountDownLatch latch;
|
||||
private WebResourceResponse internetResponse;
|
||||
private Messenger internetHelperListener;
|
||||
|
||||
private WebViewSingleton() {
|
||||
}
|
||||
@@ -77,7 +74,7 @@ public class WebViewSingleton {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
LOG.info("internet helper service bound");
|
||||
internetHelperBound = true;
|
||||
internetHelper = new Messenger(service);
|
||||
internetHelper = IHttpService.Stub.asInterface(service);
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
@@ -104,9 +101,11 @@ public class WebViewSingleton {
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
//needed for localstorage
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
// #3373 #3424 - Fix configuration for pebble apps
|
||||
// TODO: this should use a WebViewAssetLoader
|
||||
//allow local js files access
|
||||
webSettings.setAllowContentAccess(true);
|
||||
webSettings.setAllowFileAccess(true);
|
||||
webSettings.setAllowFileAccessFromFileURLs(true);
|
||||
webSettings.setAllowUniversalAccessFromFileURLs(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,44 +113,42 @@ public class WebViewSingleton {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public WebResourceResponse send(Message webRequest) throws RemoteException, InterruptedException {
|
||||
webRequest.replyTo = internetHelperListener;
|
||||
latch = new CountDownLatch(1); //the messenger should run on a single thread, hence we don't need to be worried about concurrency. This approach however is certainly not ideal.
|
||||
internetHelper.send(webRequest);
|
||||
latch.await();
|
||||
return internetResponse;
|
||||
}
|
||||
public WebResourceResponse send(Uri webRequest) throws RemoteException, InterruptedException {
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
final HttpGetRequest httpGetRequest = new HttpGetRequest(webRequest.toString(), httpHeaders);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final Capsule<WebResourceResponse> internetResponseCapsule = new Capsule<>();
|
||||
try {
|
||||
internetHelper.get(httpGetRequest, new IHttpCallback.Stub() {
|
||||
@Override
|
||||
public void onResponse(HttpResponse response) throws RemoteException {
|
||||
response.getHeaders().addHeader("Access-Control-Allow-Origin", "*");
|
||||
WebResourceResponse internetResponse = new WebResourceResponse(
|
||||
response.getHeaders().get("content-type"),
|
||||
response.getHeaders().get("content-encoding"),
|
||||
response.getStatus(), "OK",
|
||||
response.getHeaders().toMap(),
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(response.getBody())
|
||||
);
|
||||
internetResponseCapsule.set(internetResponse);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
//Internet helper inbound (responses) handler
|
||||
private class IncomingHandler extends Handler {
|
||||
public IncomingHandler(Looper looper) {
|
||||
super(looper);
|
||||
@Override
|
||||
public void onException(String message) throws RemoteException {
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getCharsetFromHeaders(String contentType) {
|
||||
if (contentType != null && contentType.toLowerCase().trim().contains("charset=")) {
|
||||
String[] parts = contentType.toLowerCase().trim().split("=");
|
||||
if (parts.length > 0)
|
||||
return parts[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Bundle data = msg.getData();
|
||||
LOG.debug("WEBVIEW: internet helper returned: " + data.getString("response"));
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Access-Control-Allow-Origin", "*");
|
||||
|
||||
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), 200, "OK",
|
||||
headers,
|
||||
new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type")))))
|
||||
);
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
return internetResponseCapsule.get();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -162,6 +159,7 @@ public class WebViewSingleton {
|
||||
|
||||
/**
|
||||
* Checks that the webview is up and the given app is running.
|
||||
*
|
||||
* @param uuid the uuid of the application expected to be running
|
||||
* @throws IllegalStateException when the webview is not active or the app is not running
|
||||
*/
|
||||
@@ -197,24 +195,17 @@ public class WebViewSingleton {
|
||||
});
|
||||
if (contextWrapper != null && !internetHelperBound && !internetHelperInstalled) {
|
||||
String internetHelperPkg = "nodomain.freeyourgadget.internethelper";
|
||||
String internetHelperCls = internetHelperPkg + ".MyService";
|
||||
String internetHelperCls = internetHelperPkg + ".HttpService";
|
||||
try {
|
||||
contextWrapper.getPackageManager().getApplicationInfo(internetHelperPkg, 0);
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(new ComponentName(internetHelperPkg, internetHelperCls));
|
||||
contextWrapper.getApplicationContext().bindService(intent, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
internetHelperListener = new Messenger(new IncomingHandler(Looper.myLooper()));
|
||||
Looper.loop();
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
internetHelperInstalled = true;
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e) {
|
||||
|
||||
final Intent intent1 = new Intent("nodomain.freeyourgadget.internethelper.HttpService");
|
||||
intent1.setPackage("nodomain.freeyourgadget.internethelper");
|
||||
contextWrapper.getApplicationContext().bindService(intent1, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
internetHelperInstalled = false;
|
||||
LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work.");
|
||||
}
|
||||
@@ -278,6 +269,7 @@ public class WebViewSingleton {
|
||||
public interface WebViewRunnable {
|
||||
/**
|
||||
* Called in the main thread with a non-null webView webView
|
||||
*
|
||||
* @param webView the webview, never null
|
||||
*/
|
||||
void invoke(WebView webView);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.ftp;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FtpEntry implements Parcelable {
|
||||
public enum Type {
|
||||
DIRECTORY,
|
||||
FILE,
|
||||
SYMLINK,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final Type type;
|
||||
private final long size;
|
||||
private final String user;
|
||||
private final String group;
|
||||
private final long timestamp;
|
||||
private final String link;
|
||||
|
||||
protected FtpEntry(final Parcel in) {
|
||||
name = in.readString();
|
||||
type = Type.values()[in.readInt()];
|
||||
size = in.readLong();
|
||||
user = in.readString();
|
||||
group = in.readString();
|
||||
timestamp = in.readLong();
|
||||
link = in.readString();
|
||||
}
|
||||
|
||||
public FtpEntry(final String name, final Type type, final long size, final String user, final String group, final long timestamp, final String link) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.size = size;
|
||||
this.user = user;
|
||||
this.group = group;
|
||||
this.timestamp = timestamp;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public static final Creator<FtpEntry> CREATOR = new Creator<FtpEntry>() {
|
||||
@Override
|
||||
public FtpEntry createFromParcel(final Parcel in) {
|
||||
return new FtpEntry(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FtpEntry[] newArray(final int size) {
|
||||
return new FtpEntry[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeInt(type.ordinal());
|
||||
dest.writeLong(size);
|
||||
dest.writeString(user);
|
||||
dest.writeString(group);
|
||||
dest.writeLong(timestamp);
|
||||
dest.writeString(link);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package nodomain.freeyourgadget.internethelper.aidl.http;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class HttpGetRequest implements Parcelable {
|
||||
private final String url;
|
||||
private final HttpHeaders headers;
|
||||
|
||||
protected HttpGetRequest(final Parcel in) {
|
||||
url = in.readString();
|
||||
headers = in.readParcelable(HttpGetRequest.class.getClassLoader());
|
||||
}
|
||||
|
||||
public HttpGetRequest(String url, HttpHeaders headers) {
|
||||
this.url = url;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public static final Creator<HttpGetRequest> CREATOR = new Creator<HttpGetRequest>() {
|
||||
@Override
|
||||
public HttpGetRequest createFromParcel(final Parcel in) {
|
||||
return new HttpGetRequest(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpGetRequest[] newArray(final int size) {
|
||||
return new HttpGetRequest[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
|
||||
dest.writeString(url);
|
||||
dest.writeParcelable(headers, 0);
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public HttpHeaders getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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() {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user