1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 04:46:51 +01:00

Pebble: Make background JS support toggle-able

- Add preference to enable background JS (default disabled)
- Remove the dummy activity used to create the webview, use ExternalPebbleJSActivity instead
- Add layout for legacy configuration, used if background JS is not enabled
- Create the view upon connecting, not when launching the application
- Remove the generic helpers used to find out if any device would need the background webview
- Drastic refactoring of WebviewSingleton moving internal classes in a new package "webview" in service/devices/pebble
This commit is contained in:
Daniele Gobbetti 2017-09-25 17:12:35 +02:00
parent af6271a428
commit e8ae47de79
18 changed files with 635 additions and 558 deletions

View File

@ -362,10 +362,6 @@
android:name=".activities.DiscoveryActivity"
android:label="@string/title_activity_discovery"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.BackgroundWebViewActivity"
android:theme="@android:style/Theme.Translucent"
android:label="@string/activity_web_view"/>
<activity
android:name=".activities.AndroidPairingActivity"
android:label="@string/title_activity_android_pairing" />

View File

@ -47,7 +47,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import nodomain.freeyourgadget.gadgetbridge.activities.BackgroundWebViewActivity;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
@ -58,7 +57,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -167,8 +165,6 @@ public class GBApplication extends Application {
String language = prefs.getString("language", "default");
setLanguage(language);
createWebViewActivity();
deviceService = createDeviceService();
loadAppsBlackList();
loadCalendarsBlackList();
@ -180,14 +176,6 @@ public class GBApplication extends Application {
}
}
private void createWebViewActivity() {
if (DeviceHelper.getInstance().needsBackgroundWebView(this)) {
Intent intent = new Intent(getContext(), BackgroundWebViewActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);

View File

@ -1,15 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
public class BackgroundWebViewActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebViewSingleton.getInstance(this);
finish();
}
}

View File

@ -24,7 +24,9 @@ import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.Toast;
@ -34,9 +36,13 @@ import org.slf4j.LoggerFactory;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
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.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
@ -46,41 +52,77 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
private Uri confUri;
private WebView myWebView;
public static final String START_BG_WEBVIEW = "start_webview";
public static final String SHOW_CONFIG = "configure";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GBDevice currentDevice;
UUID currentUUID;
Bundle extras = getIntent().getExtras();
if (extras != null) {
WebViewSingleton.runJavascriptInterface((GBDevice) extras.getParcelable(GBDevice.EXTRA_DEVICE), (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID));
currentDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
currentUUID = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
if (GBApplication.getPrefs().getBoolean("pebble_enable_background_javascript", false)) {
if (extras.getBoolean(SHOW_CONFIG, false)) {
WebViewSingleton.runJavascriptInterface(currentDevice, currentUUID);
} else if (extras.getBoolean(START_BG_WEBVIEW, false)) {
WebViewSingleton.getInstance(this);
finish();
}
}
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
setContentView(R.layout.activity_external_pebble_js);
WebViewSingleton.updateActivityContext(this);
myWebView = WebViewSingleton.getWebView();
myWebView.setWillNotDraw(false);
myWebView.removeJavascriptInterface("GBActivity");
myWebView.addJavascriptInterface(new ActivityJSInterface(ExternalPebbleJSActivity.this), "GBActivity");
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.addView(myWebView);
myWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
if (GBApplication.getPrefs().getBoolean("pebble_enable_background_javascript", false)) {
setContentView(R.layout.activity_external_pebble_js);
myWebView = WebViewSingleton.getWebView(this);
if (myWebView.getParent() != null) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
myWebView.setWillNotDraw(false);
myWebView.removeJavascriptInterface("GBActivity");
myWebView.addJavascriptInterface(new ActivityJSInterface(ExternalPebbleJSActivity.this), "GBActivity");
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.addView(myWebView);
@Override
public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.removeAllViews();
}
});
myWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
@Override
public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.removeAllViews();
}
});
} else {
setContentView(R.layout.activity_legacy_external_pebble_js);
myWebView = (WebView) findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient());
myWebView.setWebChromeClient(new GBChromeClient());
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
//needed to access the DOM
webSettings.setDomStorageEnabled(true);
//needed for localstorage
webSettings.setDatabaseEnabled(true);
JSInterface gbJSInterface = new JSInterface(currentDevice, currentUUID);
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
myWebView.addJavascriptInterface(new ActivityJSInterface(ExternalPebbleJSActivity.this), "GBActivity");
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
}
}
@Override
@ -119,7 +161,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
Context mContext;
public ActivityJSInterface(Context c) {
ActivityJSInterface(Context c) {
mContext = c;
}

View File

@ -424,6 +424,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startIntent.putExtra(ExternalPebbleJSActivity.SHOW_CONFIG, true);
startActivity(startIntent);
return true;
case R.id.appmanager_app_openinstore:

View File

@ -120,11 +120,6 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@Override
public boolean needsBackgroundWebView(GBDevice device) {
return false;
}
@Override
public int getBondingStyle(GBDevice device) {
return BONDING_STYLE_ASK;

View File

@ -220,14 +220,6 @@ public interface DeviceCoordinator {
*/
Class<? extends Activity> getAppsManagementActivity();
/**
* Returns true if the given device needs a background webview for
* executing javascript or configuration, for example.
*
* @param device
*/
boolean needsBackgroundWebView(GBDevice device);
/**
* Returns how/if the given device should be bonded before connecting to it.
* @param device

View File

@ -25,7 +25,6 @@ import android.support.annotation.NonNull;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
@ -157,11 +156,6 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public boolean needsBackgroundWebView(GBDevice device) {
return true;
}
@Override
public boolean supportsRealtimeData() {
return false;

View File

@ -43,6 +43,7 @@ import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
@ -161,6 +162,14 @@ class PebbleIoThread extends GBDeviceIoThread {
mBtSocket.connect();
mInStream = mBtSocket.getInputStream();
mOutStream = mBtSocket.getOutputStream();
if (prefs.getBoolean("pebble_enable_background_javascript", false)) {
Intent startIntent = new Intent(getContext(), ExternalPebbleJSActivity.class);
startIntent.putExtra(ExternalPebbleJSActivity.START_BG_WEBVIEW, true);
getContext().startActivity(startIntent);
} else {
LOG.debug("Not enabling background Webview, is disabled in preferences.");
}
}
}
} catch (IOException e) {
@ -380,7 +389,9 @@ class PebbleIoThread extends GBDeviceIoThread {
gbDevice.setState(GBDevice.State.WAITING_FOR_RECONNECT);
}
WebViewSingleton.disposeWebView();
if (prefs.getBoolean("pebble_enable_background_javascript", false)) {
WebViewSingleton.disposeWebView();
}
gbDevice.sendDeviceUpdateIntent(getContext());
}
@ -506,7 +517,9 @@ class PebbleIoThread extends GBDeviceIoThread {
break;
case START:
LOG.info("got GBDeviceEventAppManagement START event for uuid: " + appMgmt.uuid);
WebViewSingleton.runJavascriptInterface(gbDevice, appMgmt.uuid);
if (prefs.getBoolean("pebble_enable_background_javascript", false)) {
WebViewSingleton.runJavascriptInterface(gbDevice, appMgmt.uuid);
}
break;
default:
break;
@ -518,7 +531,9 @@ class PebbleIoThread extends GBDeviceIoThread {
setInstallSlot(appInfoEvent.freeSlot);
return false;
} else if (deviceEvent instanceof GBDeviceEventAppMessage) {
sendAppMessageJS((GBDeviceEventAppMessage) deviceEvent);
if (GBApplication.getPrefs().getBoolean("pebble_enable_background_javascript", false)) {
sendAppMessageJS((GBDeviceEventAppMessage) deviceEvent);
}
if (mEnablePebblekit) {
LOG.info("Got AppMessage event");
if (mPebbleKitSupport != null && ((GBDeviceEventAppMessage) deviceEvent).type == GBDeviceEventAppMessage.TYPE_APPMESSAGE) {

View File

@ -0,0 +1,61 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.support.v4.app.ActivityCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
class CurrentPosition {
private static final Logger LOG = LoggerFactory.getLogger(CurrentPosition.class);
long timestamp;
double altitude;
float latitude, longitude, accuracy, speed;
float getLatitude() {
return latitude;
}
float getLongitude() {
return longitude;
}
CurrentPosition() {
Prefs prefs = GBApplication.getPrefs();
this.latitude = prefs.getFloat("location_latitude", 0);
this.longitude = prefs.getFloat("location_longitude", 0);
LOG.info("got longitude/latitude from preferences: " + latitude + "/" + longitude);
this.timestamp = System.currentTimeMillis() - 86400000; //let accessor know this value is really old
if (ActivityCompat.checkSelfPermission(GBApplication.getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
prefs.getBoolean("use_updated_location_if_available", false)) {
LocationManager locationManager = (LocationManager) GBApplication.getContext().getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
if (lastKnownLocation != null) {
this.timestamp = lastKnownLocation.getTime();
this.timestamp = System.currentTimeMillis() - 1000; //TODO: request updating the location and don't fake its age
this.latitude = (float) lastKnownLocation.getLatitude();
this.longitude = (float) lastKnownLocation.getLongitude();
this.accuracy = lastKnownLocation.getAccuracy();
this.altitude = (float) lastKnownLocation.getAltitude();
this.speed = lastKnownLocation.getSpeed();
}
}
}
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview;
import android.webkit.ConsoleMessage;
import android.webkit.WebChromeClient;
import android.widget.Toast;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GBChromeClient extends WebChromeClient {
@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
}
return super.onConsoleMessage(consoleMessage);
}
private static String formatConsoleMessage(ConsoleMessage message) {
String sourceId = message.sourceId();
if (sourceId == null || sourceId.length() == 0) {
sourceId = "unknown";
}
return String.format("%s (at %s: %d)", message.message(), sourceId, message.lineNumber());
}
}

View File

@ -0,0 +1,204 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview;
import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.RemoteException;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import static nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton.internetHelper;
import static nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton.internetHelperBound;
import static nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton.internetHelperListener;
import static nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton.internetResponse;
import static nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton.latch;
public class GBWebClient extends WebViewClient {
private static final Logger LOG = LoggerFactory.getLogger(GBWebClient.class);
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Uri parsedUri = request.getUrl();
LOG.debug("WEBVIEW shouldInterceptRequest URL: " + parsedUri.toString());
WebResourceResponse mimickedReply = mimicReply(parsedUri);
if (mimickedReply != null)
return mimickedReply;
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
LOG.debug("WEBVIEW shouldInterceptRequest URL (legacy): " + url);
Uri parsedUri = Uri.parse(url);
WebResourceResponse mimickedReply = mimicReply(parsedUri);
if (mimickedReply != null)
return mimickedReply;
return super.shouldInterceptRequest(view, url);
}
private WebResourceResponse mimicReply(Uri requestedUri) {
if (requestedUri.getHost() != null && (requestedUri.getHost().contains("openweathermap.org") || requestedUri.getHost().contains("tagesschau.de"))) {
if (internetHelperBound) {
LOG.debug("WEBVIEW forwarding request to the internet helper");
Bundle bundle = new Bundle();
bundle.putString("URL", requestedUri.toString());
Message webRequest = Message.obtain();
webRequest.replyTo = internetHelperListener;
webRequest.setData(bundle);
try {
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;
} catch (RemoteException | InterruptedException e) {
e.printStackTrace();
}
} else {
LOG.debug("WEBVIEW request to openweathermap.org detected of type: " + requestedUri.getPath() + " params: " + requestedUri.getQuery());
return mimicOpenWeatherMapResponse(requestedUri.getPath(), requestedUri.getQueryParameter("units"));
}
} else {
LOG.debug("WEBVIEW request:" + requestedUri.toString() + " not intercepted");
}
return null;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri parsedUri = Uri.parse(url);
if (parsedUri.getScheme().startsWith("http")) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
GBApplication.getContext().startActivity(i);
} else if (parsedUri.getScheme().startsWith("pebblejs")) {
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
view.loadUrl(url);
} else if (parsedUri.getScheme().equals("data")) { //clay
view.loadUrl(url);
} else {
LOG.debug("WEBVIEW Ignoring unhandled scheme: " + parsedUri.getScheme());
}
return true;
}
private static WebResourceResponse mimicOpenWeatherMapResponse(String type, String units) {
if (Weather.getInstance() == null || Weather.getInstance().getWeather2() == null) {
LOG.warn("WEBVIEW - Weather instance is null, cannot update weather");
return null;
}
CurrentPosition currentPosition = new CurrentPosition();
try {
JSONObject resp;
if ("/data/2.5/weather".equals(type) && Weather.getInstance().getWeather2().reconstructedWeather != null) {
resp = new JSONObject(Weather.getInstance().getWeather2().reconstructedWeather.toString());
JSONObject main = resp.getJSONObject("main");
convertTemps(main, units); //caller might want different units
resp.put("cod", 200);
resp.put("coord", coordObject(currentPosition));
resp.put("sys", sysObject(currentPosition));
// } else if ("/data/2.5/forecast".equals(type) && Weather.getInstance().getWeather2().reconstructedForecast != null) { //this is wrong, as we only have daily data. Unfortunately it looks like daily forecasts cannot be reconstructed
// resp = new JSONObject(Weather.getInstance().getWeather2().reconstructedForecast.toString());
//
// JSONObject city = resp.getJSONObject("city");
// city.put("coord", coordObject(currentPosition));
//
// JSONArray list = resp.getJSONArray("list");
// for (int i = 0, size = list.length(); i < size; i++) {
// JSONObject item = list.getJSONObject(i);
// JSONObject main = item.getJSONObject("main");
// convertTemps(main, units); //caller might want different units
// }
//
// resp.put("cod", 200);
} else {
LOG.warn("WEBVIEW - cannot mimick request of type " + type + " (unsupported or lack of data)");
return null;
}
LOG.info("WEBVIEW - mimic openweather response" + resp.toString());
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new WebResourceResponse("application/json", "utf-8", 200, "OK",
headers,
new ByteArrayInputStream(resp.toString().getBytes())
);
} else {
return new WebResourceResponse("application/json", "utf-8", new ByteArrayInputStream(resp.toString().getBytes()));
}
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
return null;
}
private static JSONObject sysObject(CurrentPosition currentPosition) throws JSONException {
GregorianCalendar[] sunrise = SPA.calculateSunriseTransitSet(new GregorianCalendar(), currentPosition.getLatitude(), currentPosition.getLongitude(), DeltaT.estimate(new GregorianCalendar()));
JSONObject sys = new JSONObject();
sys.put("country", "World");
sys.put("sunrise", (sunrise[0].getTimeInMillis() / 1000));
sys.put("sunset", (sunrise[2].getTimeInMillis() / 1000));
return sys;
}
private static void convertTemps(JSONObject main, String units) throws JSONException {
if ("metric".equals(units)) {
main.put("temp", (int) main.get("temp") - 273);
main.put("temp_min", (int) main.get("temp_min") - 273);
main.put("temp_max", (int) main.get("temp_max") - 273);
} else if ("imperial".equals(units)) { //it's 2017... this is so sad
main.put("temp", ((int) (main.get("temp")) - 273.15f) * 1.8f + 32);
main.put("temp_min", ((int) (main.get("temp_min")) - 273.15f) * 1.8f + 32);
main.put("temp_max", ((int) (main.get("temp_max")) - 273.15f) * 1.8f + 32);
}
}
private static JSONObject coordObject(CurrentPosition currentPosition) throws JSONException {
JSONObject coord = new JSONObject();
coord.put("lat", currentPosition.getLatitude());
coord.put("lon", currentPosition.getLongitude());
return coord;
}
}

View File

@ -0,0 +1,233 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Scanner;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
public class JSInterface {
private UUID mUuid;
private GBDevice device;
private Integer lastTransaction;
private static final Logger LOG = LoggerFactory.getLogger(JSInterface.class);
public JSInterface(GBDevice device, UUID mUuid) {
LOG.debug("Creating JS interface for UUID: " + mUuid.toString());
this.device = device;
this.mUuid = mUuid;
this.lastTransaction = 0;
}
private boolean isLocationEnabledForWatchApp() {
return true; //FIXME: as long as we don't give watchapp internet access it's not a problem
}
@JavascriptInterface
public void gbLog(String msg) {
LOG.debug("WEBVIEW webpage log: " + msg);
}
@JavascriptInterface
public String sendAppMessage(String msg, String needsTransactionMsg) {
boolean needsTransaction = "true".equals(needsTransactionMsg);
LOG.debug("from WEBVIEW: " + msg + " needs a transaction: " + needsTransaction);
JSONObject knownKeys = WebViewSingleton.getAppConfigurationKeys(this.mUuid);
try {
JSONObject in = new JSONObject(msg);
JSONObject out = new JSONObject();
String inKey, outKey;
boolean passKey;
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
passKey = false;
inKey = key.next();
outKey = null;
int pebbleAppIndex = knownKeys.optInt(inKey, -1);
if (pebbleAppIndex != -1) {
passKey = true;
outKey = String.valueOf(pebbleAppIndex);
} else {
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
Scanner scanner = new Scanner(inKey);
if (scanner.hasNextInt() && inKey.equals("" + scanner.nextInt())) {
passKey = true;
outKey = inKey;
}
}
if (passKey) {
Object obj = in.get(inKey);
out.put(outKey, obj);
} else {
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);
}
}
LOG.info("WEBVIEW message to pebble: " + out.toString());
if (needsTransaction) {
this.lastTransaction++;
GBApplication.deviceService().onAppConfiguration(this.mUuid, out.toString(), this.lastTransaction);
return this.lastTransaction.toString();
} else {
GBApplication.deviceService().onAppConfiguration(this.mUuid, out.toString(), null);
}
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public String getActiveWatchInfo() {
JSONObject wi = new JSONObject();
try {
wi.put("firmware", device.getFirmwareVersion());
wi.put("platform", PebbleUtils.getPlatformName(device.getModel()));
wi.put("model", PebbleUtils.getModel(device.getModel()));
//TODO: use real info
wi.put("language", "en");
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
//Json not supported apparently, we need to cast back and forth
return wi.toString();
}
@JavascriptInterface
public String getAppConfigurationFile() {
LOG.debug("WEBVIEW loading config file of " + this.mUuid.toString());
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, this.mUuid.toString() + "_config.js");
if (configurationFile.exists()) {
return "file:///" + configurationFile.getAbsolutePath();
}
} catch (IOException e) {
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public String getAppStoredPreset() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, this.mUuid.toString() + "_preset.json");
if (configurationFile.exists()) {
return FileUtils.getStringFromFile(configurationFile);
}
} catch (IOException e) {
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public void saveAppStoredPreset(String msg) {
Writer writer;
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File presetsFile = new File(destDir, this.mUuid.toString() + "_preset.json");
writer = new BufferedWriter(new FileWriter(presetsFile));
writer.write(msg);
writer.close();
GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
} catch (IOException e) {
GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
LOG.warn(e.getMessage());
}
}
@JavascriptInterface
public String getAppUUID() {
return this.mUuid.toString();
}
@JavascriptInterface
public String getAppLocalstoragePrefix() {
String prefix = device.getAddress() + this.mUuid.toString();
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = prefix.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
final StringBuilder sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(String.format("%02X", aByte));
}
return sb.toString().toLowerCase();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
LOG.warn(e.getMessage());
return prefix;
}
}
@JavascriptInterface
public String getWatchToken() {
//specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
return "gb" + this.mUuid.toString();
}
@JavascriptInterface
public String getCurrentPosition() {
if (!isLocationEnabledForWatchApp()) {
return "";
}
//we need to override this because the coarse location is not enough for the android webview, we should add the permission for fine location.
JSONObject geoPosition = new JSONObject();
JSONObject coords = new JSONObject();
try {
CurrentPosition currentPosition = new CurrentPosition();
geoPosition.put("timestamp", currentPosition.timestamp);
coords.put("latitude", currentPosition.latitude);
coords.put("longitude", currentPosition.longitude);
coords.put("accuracy", currentPosition.accuracy);
coords.put("altitude", currentPosition.altitude);
coords.put("speed", currentPosition.speed);
geoPosition.put("coords", coords);
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
LOG.info("WEBVIEW - geo position" + geoPosition.toString());
return geoPosition.toString();
}
@JavascriptInterface
public void eventFinished(String event) {
LOG.debug("WEBVIEW event finished: " + event);
}
}

View File

@ -281,19 +281,4 @@ public class DeviceHelper {
return false;
}
/**
* Returns true if the background webview for executing javascript is needed
* for any of the known/available devices.
* @param context
*/
public boolean needsBackgroundWebView(Context context) {
Set<GBDevice> availableDevices = getAvailableDevices(context);
for (GBDevice device : availableDevices) {
DeviceCoordinator coordinator = getCoordinator(device);
if (coordinator.needsBackgroundWebView(device)) {
return true;
}
}
return false;
}
}

View File

@ -1,18 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.MutableContextWrapper;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@ -20,23 +13,12 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.util.SparseArray;
import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
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;
import android.widget.Toast;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
import org.json.JSONArray;
import org.json.JSONException;
@ -44,27 +26,20 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
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;
public class WebViewSingleton {
@ -75,11 +50,11 @@ public class WebViewSingleton {
private Looper mainLooper;
private static WebViewSingleton webViewSingleton = new WebViewSingleton();
private static UUID currentRunningUUID;
private static Messenger internetHelper = null;
private static boolean internetHelperBound;
private static CountDownLatch latch;
private static WebResourceResponse internetResponse;
private final static Messenger internetHelperListener = new Messenger(new IncomingHandler());
public static Messenger internetHelper = null;
public static boolean internetHelperBound;
public static CountDownLatch latch;
public static WebResourceResponse internetResponse;
public final static Messenger internetHelperListener = new Messenger(new IncomingHandler());
private WebViewSingleton() {
}
@ -120,7 +95,7 @@ public class WebViewSingleton {
};
//Internet helper inbound (responses) handler
static class IncomingHandler extends Handler {
private static class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Bundle data = msg.getData();
@ -141,14 +116,9 @@ public class WebViewSingleton {
}
}
public static void updateActivityContext(Activity context) {
if (context != null) {
webViewSingleton.contextWrapper.setBaseContext(context);
}
}
@NonNull
public static WebView getWebView() {
public static WebView getWebView(Context context) {
webViewSingleton.contextWrapper.setBaseContext(context);
return webViewSingleton.instance;
}
@ -242,229 +212,7 @@ public class WebViewSingleton {
});
}
private static class CurrentPosition {
long timestamp;
double altitude;
float latitude, longitude, accuracy, speed;
private CurrentPosition() {
Prefs prefs = GBApplication.getPrefs();
this.latitude = prefs.getFloat("location_latitude", 0);
this.longitude = prefs.getFloat("location_longitude", 0);
LOG.info("got longitude/latitude from preferences: " + latitude + "/" + longitude);
this.timestamp = System.currentTimeMillis() - 86400000; //let accessor know this value is really old
if (ActivityCompat.checkSelfPermission(GBApplication.getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
prefs.getBoolean("use_updated_location_if_available", false)) {
LocationManager locationManager = (LocationManager) GBApplication.getContext().getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
if (lastKnownLocation != null) {
this.timestamp = lastKnownLocation.getTime();
this.timestamp = System.currentTimeMillis() - 1000; //TODO: request updating the location and don't fake its age
this.latitude = (float) lastKnownLocation.getLatitude();
this.longitude = (float) lastKnownLocation.getLongitude();
this.accuracy = lastKnownLocation.getAccuracy();
this.altitude = (float) lastKnownLocation.getAltitude();
this.speed = lastKnownLocation.getSpeed();
}
}
}
}
}
private static WebResourceResponse mimicKiezelPayResponse() {
return null;
}
private static JSONObject coordObject(CurrentPosition currentPosition) throws JSONException {
JSONObject coord = new JSONObject();
coord.put("lat", currentPosition.latitude);
coord.put("lon", currentPosition.longitude);
return coord;
}
private static JSONObject sysObject(CurrentPosition currentPosition) throws JSONException {
GregorianCalendar[] sunrise = SPA.calculateSunriseTransitSet(new GregorianCalendar(), currentPosition.latitude, currentPosition.longitude, DeltaT.estimate(new GregorianCalendar()));
JSONObject sys = new JSONObject();
sys.put("country", "World");
sys.put("sunrise", (sunrise[0].getTimeInMillis() / 1000));
sys.put("sunset", (sunrise[2].getTimeInMillis() / 1000));
return sys;
}
private static void convertTemps(JSONObject main, String units) throws JSONException {
if ("metric".equals(units)) {
main.put("temp", (int) main.get("temp") - 273);
main.put("temp_min", (int) main.get("temp_min") - 273);
main.put("temp_max", (int) main.get("temp_max") - 273);
} else if ("imperial".equals(units)) { //it's 2017... this is so sad
main.put("temp", ((int) (main.get("temp")) - 273.15f) * 1.8f + 32);
main.put("temp_min", ((int) (main.get("temp_min")) - 273.15f) * 1.8f + 32);
main.put("temp_max", ((int) (main.get("temp_max")) - 273.15f) * 1.8f + 32);
}
}
private static WebResourceResponse mimicOpenWeatherMapResponse(String type, String units) {
if (Weather.getInstance() == null || Weather.getInstance().getWeather2() == null) {
LOG.warn("WEBVIEW - Weather instance is null, cannot update weather");
return null;
}
CurrentPosition currentPosition = new CurrentPosition();
try {
JSONObject resp;
if ("/data/2.5/weather".equals(type) && Weather.getInstance().getWeather2().reconstructedWeather != null) {
resp = new JSONObject(Weather.getInstance().getWeather2().reconstructedWeather.toString());
JSONObject main = resp.getJSONObject("main");
convertTemps(main, units); //caller might want different units
resp.put("cod", 200);
resp.put("coord", coordObject(currentPosition));
resp.put("sys", sysObject(currentPosition));
// } else if ("/data/2.5/forecast".equals(type) && Weather.getInstance().getWeather2().reconstructedForecast != null) { //this is wrong, as we only have daily data. Unfortunately it looks like daily forecasts cannot be reconstructed
// resp = new JSONObject(Weather.getInstance().getWeather2().reconstructedForecast.toString());
//
// JSONObject city = resp.getJSONObject("city");
// city.put("coord", coordObject(currentPosition));
//
// JSONArray list = resp.getJSONArray("list");
// for (int i = 0, size = list.length(); i < size; i++) {
// JSONObject item = list.getJSONObject(i);
// JSONObject main = item.getJSONObject("main");
// convertTemps(main, units); //caller might want different units
// }
//
// resp.put("cod", 200);
} else {
LOG.warn("WEBVIEW - cannot mimick request of type " + type + " (unsupported or lack of data)");
return null;
}
LOG.info("WEBVIEW - mimic openweather response" + resp.toString());
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new WebResourceResponse("application/json", "utf-8", 200, "OK",
headers,
new ByteArrayInputStream(resp.toString().getBytes())
);
} else {
return new WebResourceResponse("application/json", "utf-8", new ByteArrayInputStream(resp.toString().getBytes()));
}
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
return null;
}
private static class GBChromeClient extends WebChromeClient {
@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
}
return super.onConsoleMessage(consoleMessage);
}
}
private static String formatConsoleMessage(ConsoleMessage message) {
String sourceId = message.sourceId();
if (sourceId == null || sourceId.length() == 0) {
sourceId = "unknown";
}
return String.format("%s (at %s: %d)", message.message(), sourceId, message.lineNumber());
}
private static class GBWebClient extends WebViewClient {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Uri parsedUri = request.getUrl();
LOG.debug("WEBVIEW shouldInterceptRequest URL: " + parsedUri.toString());
WebResourceResponse mimickedReply = mimicReply(parsedUri);
if (mimickedReply != null)
return mimickedReply;
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
LOG.debug("WEBVIEW shouldInterceptRequest URL (legacy): " + url);
Uri parsedUri = Uri.parse(url);
WebResourceResponse mimickedReply = mimicReply(parsedUri);
if (mimickedReply != null)
return mimickedReply;
return super.shouldInterceptRequest(view, url);
}
private WebResourceResponse mimicReply(Uri requestedUri) {
if (requestedUri.getHost() != null && (requestedUri.getHost().contains("openweathermap.org") || requestedUri.getHost().contains("tagesschau.de") )) {
if (internetHelperBound) {
LOG.debug("WEBVIEW forwarding request to the internet helper");
Bundle bundle = new Bundle();
bundle.putString("URL", requestedUri.toString());
Message webRequest = Message.obtain();
webRequest.replyTo = internetHelperListener;
webRequest.setData(bundle);
try {
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;
} catch (RemoteException | InterruptedException e) {
e.printStackTrace();
}
} else {
LOG.debug("WEBVIEW request to openweathermap.org detected of type: " + requestedUri.getPath() + " params: " + requestedUri.getQuery());
return mimicOpenWeatherMapResponse(requestedUri.getPath(), requestedUri.getQueryParameter("units"));
}
} else {
LOG.debug("WEBVIEW request:" + requestedUri.toString() + " not intercepted");
}
return null;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri parsedUri = Uri.parse(url);
if (parsedUri.getScheme().startsWith("http")) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
GBApplication.getContext().startActivity(i);
} else if (parsedUri.getScheme().startsWith("pebblejs")) {
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
view.loadUrl(url);
} else if (parsedUri.getScheme().equals("data")) { //clay
view.loadUrl(url);
} else {
LOG.debug("WEBVIEW Ignoring unhandled scheme: " + parsedUri.getScheme());
}
return true;
}
}
private static JSONObject getAppConfigurationKeys(UUID uuid) {
public static JSONObject getAppConfigurationKeys(UUID uuid) {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, uuid.toString() + ".json");
@ -525,207 +273,4 @@ public class WebViewSingleton {
return jsAppMessage.toString();
}
private static class JSInterface {
UUID mUuid;
GBDevice device;
Integer lastTransaction;
private JSInterface(GBDevice device, UUID mUuid) {
LOG.debug("Creating JS interface for UUID: " + mUuid.toString());
this.device = device;
this.mUuid = mUuid;
this.lastTransaction = 0;
}
private boolean isLocationEnabledForWatchApp() {
return true; //as long as we don't give watchapp internet access it's not a problem
}
@JavascriptInterface
public void gbLog(String msg) {
LOG.debug("WEBVIEW webpage log: " + msg);
}
@JavascriptInterface
public String sendAppMessage(String msg, String needsTransactionMsg) {
boolean needsTransaction = "true".equals(needsTransactionMsg);
LOG.debug("from WEBVIEW: " + msg + " needs a transaction: " + needsTransaction);
JSONObject knownKeys = getAppConfigurationKeys(this.mUuid);
try {
JSONObject in = new JSONObject(msg);
JSONObject out = new JSONObject();
String inKey, outKey;
boolean passKey;
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
passKey = false;
inKey = key.next();
outKey = null;
int pebbleAppIndex = knownKeys.optInt(inKey, -1);
if (pebbleAppIndex != -1) {
passKey = true;
outKey = String.valueOf(pebbleAppIndex);
} else {
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
Scanner scanner = new Scanner(inKey);
if (scanner.hasNextInt() && inKey.equals("" + scanner.nextInt())) {
passKey = true;
outKey = inKey;
}
}
if (passKey) {
Object obj = in.get(inKey);
out.put(outKey, obj);
} else {
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);
}
}
LOG.info("WEBVIEW message to pebble: " + out.toString());
if (needsTransaction) {
this.lastTransaction++;
GBApplication.deviceService().onAppConfiguration(this.mUuid, out.toString(), this.lastTransaction);
return this.lastTransaction.toString();
} else {
GBApplication.deviceService().onAppConfiguration(this.mUuid, out.toString(), null);
}
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public String getActiveWatchInfo() {
JSONObject wi = new JSONObject();
try {
wi.put("firmware", device.getFirmwareVersion());
wi.put("platform", PebbleUtils.getPlatformName(device.getModel()));
wi.put("model", PebbleUtils.getModel(device.getModel()));
//TODO: use real info
wi.put("language", "en");
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
//Json not supported apparently, we need to cast back and forth
return wi.toString();
}
@JavascriptInterface
public String getAppConfigurationFile() {
LOG.debug("WEBVIEW loading config file of " + this.mUuid.toString());
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, this.mUuid.toString() + "_config.js");
if (configurationFile.exists()) {
return "file:///" + configurationFile.getAbsolutePath();
}
} catch (IOException e) {
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public String getAppStoredPreset() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, this.mUuid.toString() + "_preset.json");
if (configurationFile.exists()) {
return FileUtils.getStringFromFile(configurationFile);
}
} catch (IOException e) {
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
LOG.warn(e.getMessage());
}
return null;
}
@JavascriptInterface
public void saveAppStoredPreset(String msg) {
Writer writer;
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File presetsFile = new File(destDir, this.mUuid.toString() + "_preset.json");
writer = new BufferedWriter(new FileWriter(presetsFile));
writer.write(msg);
writer.close();
GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
} catch (IOException e) {
GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
LOG.warn(e.getMessage());
}
}
@JavascriptInterface
public String getAppUUID() {
return this.mUuid.toString();
}
@JavascriptInterface
public String getAppLocalstoragePrefix() {
String prefix = device.getAddress() + this.mUuid.toString();
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = prefix.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
final StringBuilder sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(String.format("%02X", aByte));
}
return sb.toString().toLowerCase();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
LOG.warn(e.getMessage());
return prefix;
}
}
@JavascriptInterface
public String getWatchToken() {
//specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
return "gb" + this.mUuid.toString();
}
@JavascriptInterface
public String getCurrentPosition() {
if (!isLocationEnabledForWatchApp()) {
return "";
}
//we need to override this because the coarse location is not enough for the android webview, we should add the permission for fine location.
JSONObject geoPosition = new JSONObject();
JSONObject coords = new JSONObject();
try {
CurrentPosition currentPosition = new CurrentPosition();
geoPosition.put("timestamp", currentPosition.timestamp);
coords.put("latitude", currentPosition.latitude);
coords.put("longitude", currentPosition.longitude);
coords.put("accuracy", currentPosition.accuracy);
coords.put("altitude", currentPosition.altitude);
coords.put("speed", currentPosition.speed);
geoPosition.put("coords", coords);
} catch (JSONException e) {
LOG.warn(e.getMessage());
}
LOG.info("WEBVIEW - geo position" + geoPosition.toString());
return geoPosition.toString();
}
@JavascriptInterface
public void eventFinished(String event) {
LOG.debug("WEBVIEW event finished: " + event);
}
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/configureWebview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />

View File

@ -170,6 +170,8 @@
<string name="pref_summary_pebble_enable_applogs">Will cause logs from watch apps to be logged by Gadgetbridge (requires reconnect)</string>
<string name="pref_title_pebble_always_ack_pebblekit">Prematurely ACK PebbleKit</string>
<string name="pref_summary_pebble_always_ack_pebblekit">Will cause messages that are sent to external 3rd party apps to be acknowledged always and immediately</string>
<string name="pref_title_pebble_enable_bgjs">Enable background JS</string>
<string name="pref_summary_pebble_enable_bgjs">When enabled, allows watchfaces to show weather, battery info etc.</string>
<string name="pref_title_pebble_reconnect_attempts">Reconnection attempts</string>

View File

@ -412,6 +412,11 @@
android:key="pebble_always_ack_pebblekit"
android:summary="@string/pref_summary_pebble_always_ack_pebblekit"
android:title="@string/pref_title_pebble_always_ack_pebblekit" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pebble_enable_background_javascript"
android:summary="@string/pref_summary_pebble_enable_bgjs"
android:title="@string/pref_title_pebble_enable_bgjs" />
<EditTextPreference
android:digits="0123456789."
android:key="pebble_emu_addr"