mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-24 16:21:16 +02: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:
parent
af6271a428
commit
e8ae47de79
@ -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" />
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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" />
|
@ -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>
|
||||
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user