mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 04:46:51 +01:00
Merge branch 'background-javascript'
This commit is contained in:
commit
6603b3fa05
@ -89,7 +89,7 @@
|
||||
<div id="step2" class="step">
|
||||
<h2>Incoming configuration data:</h2>
|
||||
<div id="jsondata"></div>
|
||||
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()">
|
||||
<button class="btn" name="send config" value="send config" onclick="Pebble.sendConfiguration()">
|
||||
Send data to pebble
|
||||
</button>
|
||||
<h2 class="store_presets">App Presets:</h2>
|
||||
|
@ -86,6 +86,21 @@ function gbPebble() {
|
||||
this.configurationURL = null;
|
||||
this.configurationValues = null;
|
||||
var self = this;
|
||||
|
||||
appMessageCallbackACK = {};
|
||||
appMessageCallbackNACK = {};
|
||||
|
||||
function appMessageCallbackProcessed(transactionId) {
|
||||
if (appMessageCallbackACK[transactionId]) {
|
||||
self.removeEventListener("ACK"+transactionId, self.appMessageCallbackACK[transactionId]);
|
||||
appMessageCallbackACK[transactionId] = undefined;
|
||||
}
|
||||
if (appMessageCallbackNACK[transactionId]) {
|
||||
self.removeEventListener("NACK"+transactionId, self.appMessageCallbackNACK[transactionId]);
|
||||
appMessageCallbackNACK[transactionId] = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
self.events = {};
|
||||
//events processing: see http://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object
|
||||
self.addEventListener = function(name, handler) {
|
||||
@ -115,6 +130,7 @@ function gbPebble() {
|
||||
for (var i = 0; i < l; i++) {
|
||||
evs[i].apply(null, args);
|
||||
}
|
||||
GBjs.eventFinished(name);
|
||||
}
|
||||
|
||||
this.actuallyOpenURL = function() {
|
||||
@ -122,9 +138,10 @@ function gbPebble() {
|
||||
window.open(self.configurationURL.toString(), "config");
|
||||
}
|
||||
|
||||
this.actuallySendData = function() {
|
||||
GBjs.sendAppMessage(self.configurationValues);
|
||||
GBjs.closeActivity();
|
||||
this.sendConfiguration = function() {
|
||||
GBjs.sendAppMessage(document.getElementById("jsondata").innerHTML, false );
|
||||
showStep("step1");
|
||||
GBActivity.closeActivity();
|
||||
}
|
||||
|
||||
this.savePreset = function() {
|
||||
@ -172,16 +189,39 @@ function gbPebble() {
|
||||
this.sendAppMessage = function (dict, callbackAck, callbackNack){
|
||||
try {
|
||||
self.configurationValues = JSON.stringify(dict);
|
||||
document.getElementById("jsondata").innerHTML=self.configurationValues;
|
||||
if (callbackAck != undefined) {
|
||||
callbackAck();
|
||||
if (document.getElementById("step2").style.display == 'block' && document.getElementById("jsondata").innerHTML == "") { //intercept the values
|
||||
document.getElementById("jsondata").innerHTML=self.configurationValues;
|
||||
} else { //pass them silently
|
||||
var needsTransaction = false;
|
||||
if (callbackAck != undefined || callbackNack != undefined) {
|
||||
needsTransaction = true;
|
||||
}
|
||||
var transactionId = GBjs.sendAppMessage(JSON.stringify(dict), needsTransaction);
|
||||
if (needsTransaction) {
|
||||
if (callbackAck != undefined) {
|
||||
self.appMessageCallbackACK[transactionId] = function(e) {
|
||||
// console.log("ACK FOR " + JSON.stringify(e));
|
||||
callbackAck(e);
|
||||
self.appMessageCallbackProcessed(transactionId);
|
||||
};
|
||||
|
||||
this.addEventListener("ACK"+transactionId, self.appMessageCallbackACK[transactionId]);
|
||||
|
||||
}
|
||||
if (callbackNack != undefined) {
|
||||
self.appMessageCallbackNACK[transactionId] = function(e) {
|
||||
// console.log("NACK FOR " + JSON.stringify(e));
|
||||
callbackNack(e);
|
||||
self.appMessageCallbackProcessed(transactionId);
|
||||
};
|
||||
|
||||
this.addEventListener("NACK"+transactionId, self.appMessageCallbackNACK[transactionId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
GBjs.gbLog("sendAppMessage failed");
|
||||
if (callbackNack != undefined) {
|
||||
callbackNack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +244,7 @@ function gbPebble() {
|
||||
|
||||
this.showConfiguration = function() {
|
||||
console.error("This watchapp doesn't support configuration");
|
||||
GBjs.closeActivity();
|
||||
GBActivity.closeActivity();
|
||||
}
|
||||
|
||||
this.parseReturnedPebbleJS = function() {
|
||||
@ -214,8 +254,8 @@ function gbPebble() {
|
||||
if (str.split(needle)[1] !== undefined) {
|
||||
var t = new Object();
|
||||
t.response = decodeURIComponent(str.split(needle)[1]);
|
||||
self.evaluate('webviewclosed',[t]);
|
||||
showStep("step2");
|
||||
self.evaluate('webviewclosed',[t]);
|
||||
} else {
|
||||
console.error("No valid configuration found in the entered string.");
|
||||
}
|
||||
@ -231,6 +271,15 @@ document.addEventListener('DOMContentLoaded', function(){
|
||||
if (jsConfigFile != null) {
|
||||
loadScript(jsConfigFile, function() {
|
||||
Pebble.evaluate('ready', [{'type': "ready"}]); //callback object apparently needed by some watchfaces
|
||||
if(document.hasFocus() && !(getURLVariable('config') == 'true')) {
|
||||
Pebble.evaluate('showConfiguration');
|
||||
} else {
|
||||
window.onfocus = function () {
|
||||
showStep("step1");
|
||||
GBjs.gbLog("window focused!!!");
|
||||
Pebble.evaluate('showConfiguration');
|
||||
};
|
||||
}
|
||||
if (getURLVariable('config') == 'true') {
|
||||
showStep("step2");
|
||||
var json_string = getURLVariable('json');
|
||||
@ -239,7 +288,6 @@ if (jsConfigFile != null) {
|
||||
if (json_string != '') {
|
||||
Pebble.evaluate('webviewclosed',[t]);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (storedPreset === undefined) {
|
||||
var presetElements = document.getElementsByClassName("load_presets");
|
||||
@ -247,7 +295,6 @@ if (jsConfigFile != null) {
|
||||
presetElements[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
Pebble.evaluate('showConfiguration');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -17,365 +17,135 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Criteria;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
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.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
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.PebbleUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
|
||||
|
||||
public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
|
||||
|
||||
private UUID appUuid;
|
||||
private Uri confUri;
|
||||
private GBDevice mGBDevice = null;
|
||||
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) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
appUuid = (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.ensureCreated(this);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
|
||||
setContentView(R.layout.activity_external_pebble_js);
|
||||
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);
|
||||
|
||||
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);
|
||||
myWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
|
||||
JSInterface gbJSInterface = new JSInterface(this);
|
||||
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
|
||||
@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);
|
||||
|
||||
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
|
||||
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
|
||||
protected void onNewIntent(Intent incoming) {
|
||||
incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
super.onNewIntent(incoming);
|
||||
confUri = incoming.getData();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
String queryString = "";
|
||||
|
||||
if (confUri != null) {
|
||||
//getting back with configuration data
|
||||
LOG.debug("WEBVIEW returned config: " + confUri.toString());
|
||||
try {
|
||||
appUuid = UUID.fromString(confUri.getHost());
|
||||
queryString = confUri.getEncodedQuery();
|
||||
} catch (IllegalArgumentException e) {
|
||||
GB.toast("returned uri: " + confUri.toString(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private JSONObject getAppConfigurationKeys() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + ".json");
|
||||
if (configurationFile.exists()) {
|
||||
String jsonstring = FileUtils.getStringFromFile(configurationFile);
|
||||
JSONObject json = new JSONObject(jsonstring);
|
||||
return json.getJSONObject("appKeys");
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isLocationEnabledForWatchApp() {
|
||||
return true; //as long as we don't give watchapp internet access it's not a problem
|
||||
}
|
||||
|
||||
private class GBChromeClient extends WebChromeClient {
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||
if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
|
||||
GB.toast(consoleMessage.message(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
//TODO: show error page
|
||||
}
|
||||
return super.onConsoleMessage(consoleMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class GBWebClient extends WebViewClient {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} else {
|
||||
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
|
||||
view.loadUrl(url);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class JSInterface {
|
||||
|
||||
Context mContext;
|
||||
|
||||
public JSInterface(Context c) {
|
||||
mContext = c;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void gbLog(String msg) {
|
||||
Log.d("WEBVIEW", msg);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void sendAppMessage(String msg) {
|
||||
LOG.debug("from WEBVIEW: " + msg);
|
||||
JSONObject knownKeys = getAppConfigurationKeys();
|
||||
|
||||
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(out.toString());
|
||||
GBApplication.deviceService().onAppConfiguration(appUuid, out.toString());
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getActiveWatchInfo() {
|
||||
JSONObject wi = new JSONObject();
|
||||
try {
|
||||
wi.put("firmware", mGBDevice.getFirmwareVersion());
|
||||
wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getModel()));
|
||||
wi.put("model", PebbleUtils.getModel(mGBDevice.getModel()));
|
||||
//TODO: use real info
|
||||
wi.put("language", "en");
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//Json not supported apparently, we need to cast back and forth
|
||||
return wi.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppConfigurationFile() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + "_config.js");
|
||||
if (configurationFile.exists()) {
|
||||
return "file:///" + configurationFile.getAbsolutePath();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppStoredPreset() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + "_preset.json");
|
||||
if (configurationFile.exists()) {
|
||||
return FileUtils.getStringFromFile(configurationFile);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void saveAppStoredPreset(String msg) {
|
||||
Writer writer;
|
||||
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File presetsFile = new File(destDir, appUuid.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);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppUUID() {
|
||||
return appUuid.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppLocalstoragePrefix() {
|
||||
String prefix = mGBDevice.getAddress() + appUuid.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 (int i = 0; i < bytes.length; i++) {
|
||||
sb.append(String.format("%02X", bytes[i]));
|
||||
}
|
||||
return sb.toString().toLowerCase();
|
||||
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
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" + appUuid.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void closeActivity() {
|
||||
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
|
||||
}
|
||||
|
||||
|
||||
@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 {
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
geoPosition.put("timestamp", (System.currentTimeMillis() / 1000) - 86400); //let JS know this value is really old
|
||||
|
||||
coords.put("latitude", prefs.getFloat("location_latitude", 0));
|
||||
coords.put("longitude", prefs.getFloat("location_longitude", 0));
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
|
||||
prefs.getBoolean("use_updated_location_if_available", false)) {
|
||||
LocationManager locationManager = (LocationManager) getApplicationContext().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) {
|
||||
geoPosition.put("timestamp", lastKnownLocation.getTime());
|
||||
|
||||
coords.put("latitude", (float) lastKnownLocation.getLatitude());
|
||||
coords.put("longitude", (float) lastKnownLocation.getLongitude());
|
||||
coords.put("accuracy", lastKnownLocation.getAccuracy());
|
||||
coords.put("altitude", lastKnownLocation.getAltitude());
|
||||
coords.put("speed", lastKnownLocation.getSpeed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
geoPosition.put("coords", coords);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return geoPosition.toString();
|
||||
}
|
||||
@Override
|
||||
protected void onNewIntent(Intent incoming) {
|
||||
incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
super.onNewIntent(incoming);
|
||||
confUri = incoming.getData();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -388,5 +158,18 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private class ActivityJSInterface {
|
||||
|
||||
Context mContext;
|
||||
|
||||
ActivityJSInterface(Context c) {
|
||||
mContext = c;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void closeActivity() {
|
||||
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -19,6 +19,11 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GBDeviceEventAppMessage extends GBDeviceEvent {
|
||||
public static int TYPE_APPMESSAGE = 0;
|
||||
public static int TYPE_ACK = 1;
|
||||
public static int TYPE_NACK = 2;
|
||||
|
||||
public int type;
|
||||
public UUID appUUID;
|
||||
public int id;
|
||||
public String message;
|
||||
|
@ -63,7 +63,7 @@ public interface EventHandler {
|
||||
|
||||
void onAppDelete(UUID uuid);
|
||||
|
||||
void onAppConfiguration(UUID appUuid, String config);
|
||||
void onAppConfiguration(UUID appUuid, String config, Integer id);
|
||||
|
||||
void onAppReorder(UUID uuids[]);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -252,10 +252,14 @@ public class GBDeviceService implements DeviceService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
Intent intent = createIntent().setAction(ACTION_APP_CONFIGURE)
|
||||
.putExtra(EXTRA_APP_UUID, uuid)
|
||||
.putExtra(EXTRA_APP_CONFIG, config);
|
||||
|
||||
if (id != null) {
|
||||
intent.putExtra(EXTRA_APP_CONFIG_ID, id);
|
||||
}
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,7 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_APP_UUID = "app_uuid";
|
||||
String EXTRA_APP_START = "app_start";
|
||||
String EXTRA_APP_CONFIG = "app_config";
|
||||
String EXTRA_APP_CONFIG_ID = "app_config_id";
|
||||
String EXTRA_URI = "uri";
|
||||
String EXTRA_CONFIG = "config";
|
||||
String EXTRA_ALARMS = "alarms";
|
||||
|
@ -105,6 +105,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ST
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG_ID;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_BOOLEAN_ENABLE;
|
||||
@ -484,7 +485,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
case ACTION_APP_CONFIGURE: {
|
||||
UUID uuid = (UUID) intent.getSerializableExtra(EXTRA_APP_UUID);
|
||||
String config = intent.getStringExtra(EXTRA_APP_CONFIG);
|
||||
mDeviceSupport.onAppConfiguration(uuid, config);
|
||||
Integer id = null;
|
||||
if (intent.hasExtra(EXTRA_APP_CONFIG_ID)) {
|
||||
id = intent.getIntExtra(EXTRA_APP_CONFIG_ID, 0);
|
||||
}
|
||||
mDeviceSupport.onAppConfiguration(uuid, config, id);
|
||||
break;
|
||||
}
|
||||
case ACTION_APP_REORDER: {
|
||||
|
@ -232,11 +232,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
if (checkBusy("app configuration")) {
|
||||
return;
|
||||
}
|
||||
delegate.onAppConfiguration(uuid, config);
|
||||
delegate.onAppConfiguration(uuid, config, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -529,7 +529,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config) {
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -352,7 +352,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config) {
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ public class LiveviewSupport extends AbstractSerialDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
|
@ -844,7 +844,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
// not supported
|
||||
}
|
||||
|
||||
|
@ -837,7 +837,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
// not supported
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config) {
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ class AppMessageHandlerHealthify extends AppMessageHandler {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
|
||||
pairs.add(new Pair<>(KEY_CONDITIONS, (Object) weatherSpec.currentCondition));
|
||||
pairs.add(new Pair<>(KEY_TEMPERATURE, (Object) (weatherSpec.currentTemp - 273)));
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
|
||||
|
||||
|
@ -44,7 +44,7 @@ class AppMessageHandlerMarioTime extends AppMessageHandler {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
|
||||
pairs.add(new Pair<>(KEY_WEATHER_ICON_ID, (Object) (byte) 1));
|
||||
pairs.add(new Pair<>(KEY_WEATHER_TEMPERATURE, (Object) (byte) (weatherSpec.currentTemp - 273)));
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
|
||||
|
||||
|
@ -96,7 +96,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
|
||||
pairs.add(new Pair<Integer, Object>(key, value));
|
||||
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -155,7 +155,7 @@ class AppMessageHandlerObsidian extends AppMessageHandler {
|
||||
pairs.add(new Pair<>(messageKeys.get("MSG_KEY_WEATHER_ICON"), (Object) getIconForConditionCode(weatherSpec.currentConditionCode, isNight))); //celsius
|
||||
pairs.add(new Pair<>(messageKeys.get("MSG_KEY_WEATHER_TEMP"), (Object) (weatherSpec.currentTemp - 273)));
|
||||
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -100,7 +100,7 @@ class AppMessageHandlerPebStyle extends AppMessageHandler {
|
||||
pairs.add(new Pair<>(KEY_WEATHER_TEMP, (Object) (weather.currentTemp - 273)));
|
||||
}
|
||||
|
||||
byte[] testMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
byte[] testMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(testMessage.length);
|
||||
|
@ -67,7 +67,7 @@ class AppMessageHandlerSquare extends AppMessageHandler {
|
||||
pairs.add(new Pair<>(CfgKeyUseCelsius, (Object) 1));
|
||||
pairs.add(new Pair<>(CfgKeyCelsiusTemperature, (Object) (weatherSpec.currentTemp - 273)));
|
||||
pairs.add(new Pair<>(CfgKeyWeatherLocation, (Object) (weatherSpec.location)));
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
|
||||
|
||||
|
@ -145,7 +145,7 @@ class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
|
||||
pairs.add(new Pair<>(messageKeys.get("WeatherForecastHighTemp"), (Object) (weatherSpec.todayMaxTemp - 273)));
|
||||
pairs.add(new Pair<>(messageKeys.get("WeatherForecastLowTemp"), (Object) (weatherSpec.todayMinTemp - 273)));
|
||||
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -91,7 +91,7 @@ class AppMessageHandlerTrekVolle extends AppMessageHandler {
|
||||
pairs.add(new Pair<>(MESSAGE_KEY_WEATHER_LOCATION, (Object) weatherSpec.location));
|
||||
|
||||
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -72,7 +72,7 @@ class AppMessageHandlerZalewszczak extends AppMessageHandler {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
|
||||
pairs.add(new Pair<Integer, Object>(KEY_TEMP, weatherSpec.currentTemp - 273 + "C"));
|
||||
pairs.add(new Pair<Integer, Object>(KEY_ICON, getIconForConditionCode(weatherSpec.currentConditionCode)));
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
|
||||
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
|
||||
|
||||
|
@ -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;
|
||||
@ -62,6 +63,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
|
||||
|
||||
class PebbleIoThread extends GBDeviceIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class);
|
||||
@ -97,6 +99,13 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
private int mBinarySize = -1;
|
||||
private int mBytesWritten = -1;
|
||||
|
||||
private void sendAppMessageJS(GBDeviceEventAppMessage appMessage) {
|
||||
WebViewSingleton.appMessage(appMessage);
|
||||
if (appMessage.type == GBDeviceEventAppMessage.TYPE_APPMESSAGE) {
|
||||
write(mPebbleProtocol.encodeApplicationMessageAck(appMessage.appUUID, (byte) appMessage.id));
|
||||
}
|
||||
}
|
||||
|
||||
PebbleIoThread(PebbleSupport pebbleSupport, GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) {
|
||||
super(gbDevice, context);
|
||||
mPebbleProtocol = (PebbleProtocol) gbDeviceProtocol;
|
||||
@ -155,6 +164,13 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
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) {
|
||||
LOG.warn("error while connecting: " + e.getMessage(), e);
|
||||
gbDevice.setState(originalState);
|
||||
@ -371,6 +387,11 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
} else {
|
||||
gbDevice.setState(GBDevice.State.WAITING_FOR_RECONNECT);
|
||||
}
|
||||
|
||||
if (prefs.getBoolean("pebble_enable_background_javascript", false)) {
|
||||
WebViewSingleton.disposeWebView();
|
||||
}
|
||||
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
@ -495,6 +516,9 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
break;
|
||||
case START:
|
||||
LOG.info("got GBDeviceEventAppManagement START event for uuid: " + appMgmt.uuid);
|
||||
if (prefs.getBoolean("pebble_enable_background_javascript", false)) {
|
||||
WebViewSingleton.runJavascriptInterface(gbDevice, appMgmt.uuid);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -506,9 +530,12 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
setInstallSlot(appInfoEvent.freeSlot);
|
||||
return false;
|
||||
} else if (deviceEvent instanceof GBDeviceEventAppMessage) {
|
||||
if (GBApplication.getPrefs().getBoolean("pebble_enable_background_javascript", false)) {
|
||||
sendAppMessageJS((GBDeviceEventAppMessage) deviceEvent);
|
||||
}
|
||||
if (mEnablePebblekit) {
|
||||
LOG.info("Got AppMessage event");
|
||||
if (mPebbleKitSupport != null) {
|
||||
if (mPebbleKitSupport != null && ((GBDeviceEventAppMessage) deviceEvent).type == GBDeviceEventAppMessage.TYPE_APPMESSAGE) {
|
||||
mPebbleKitSupport.sendAppMessageIntent((GBDeviceEventAppMessage) deviceEvent);
|
||||
}
|
||||
}
|
||||
|
@ -406,20 +406,24 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
super(device);
|
||||
mAppMessageHandlers.put(UUID_MORPHEUZ, new AppMessageHandlerMorpheuz(UUID_MORPHEUZ, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_MISFIT, new AppMessageHandlerMisfit(UUID_MISFIT, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_PEBBLE_TIMESTYLE, new AppMessageHandlerTimeStylePebble(UUID_PEBBLE_TIMESTYLE, PebbleProtocol.this));
|
||||
//mAppMessageHandlers.put(UUID_PEBSTYLE, new AppMessageHandlerPebStyle(UUID_PEBSTYLE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_MARIOTIME, new AppMessageHandlerMarioTime(UUID_MARIOTIME, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_HELTHIFY, new AppMessageHandlerHealthify(UUID_HELTHIFY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_TREKVOLLE, new AppMessageHandlerTrekVolle(UUID_TREKVOLLE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_SQUARE, new AppMessageHandlerSquare(UUID_SQUARE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_CROWEX, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_CROWEX, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_FANCY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_FANCY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_TALLY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_TALLY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_OBSIDIAN, new AppMessageHandlerObsidian(UUID_OBSIDIAN, PebbleProtocol.this));
|
||||
if (!(GBApplication.getPrefs().getBoolean("pebble_enable_background_javascript", false))) {
|
||||
mAppMessageHandlers.put(UUID_PEBBLE_TIMESTYLE, new AppMessageHandlerTimeStylePebble(UUID_PEBBLE_TIMESTYLE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_PEBSTYLE, new AppMessageHandlerPebStyle(UUID_PEBSTYLE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_MARIOTIME, new AppMessageHandlerMarioTime(UUID_MARIOTIME, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_HELTHIFY, new AppMessageHandlerHealthify(UUID_HELTHIFY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_TREKVOLLE, new AppMessageHandlerTrekVolle(UUID_TREKVOLLE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_SQUARE, new AppMessageHandlerSquare(UUID_SQUARE, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_CROWEX, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_CROWEX, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_FANCY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_FANCY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_TALLY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_TALLY, PebbleProtocol.this));
|
||||
mAppMessageHandlers.put(UUID_OBSIDIAN, new AppMessageHandlerObsidian(UUID_OBSIDIAN, PebbleProtocol.this));
|
||||
}
|
||||
}
|
||||
|
||||
private final HashMap<Byte, DatalogSession> mDatalogSessions = new HashMap<>();
|
||||
|
||||
private Integer[] idLookup = new Integer[256];
|
||||
|
||||
private byte[] encodeSimpleMessage(short endpoint, byte command) {
|
||||
final short LENGTH_SIMPLEMESSAGE = 1;
|
||||
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SIMPLEMESSAGE);
|
||||
@ -1472,7 +1476,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
|
||||
int param = start ? 1 : 0;
|
||||
pairs.add(new Pair<>(1, (Object) param));
|
||||
return encodeApplicationMessagePush(ENDPOINT_LAUNCHER, uuid, pairs);
|
||||
return encodeApplicationMessagePush(ENDPOINT_LAUNCHER, uuid, pairs, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1905,7 +1909,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
return new GBDeviceEvent[]{appMessage, sendBytesAck};
|
||||
}
|
||||
|
||||
byte[] encodeApplicationMessagePush(short endpoint, UUID uuid, ArrayList<Pair<Integer, Object>> pairs) {
|
||||
byte[] encodeApplicationMessagePush(short endpoint, UUID uuid, ArrayList<Pair<Integer, Object>> pairs, Integer ext_id) {
|
||||
int length = LENGTH_UUID + 3; // UUID + (PUSH + id + length of dict)
|
||||
for (Pair<Integer, Object> pair : pairs) {
|
||||
if (pair.first == null || pair.second == null)
|
||||
@ -1967,6 +1971,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
idLookup[last_id] = ext_id;
|
||||
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
@ -2006,7 +2012,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
return encodeApplicationMessagePush(ENDPOINT_APPLICATIONMESSAGE, uuid, pairs);
|
||||
return encodeApplicationMessagePush(ENDPOINT_APPLICATIONMESSAGE, uuid, pairs, null);
|
||||
}
|
||||
|
||||
private byte reverseBits(byte in) {
|
||||
@ -2604,12 +2610,24 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
}
|
||||
break;
|
||||
case APPLICATIONMESSAGE_ACK:
|
||||
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") ACK");
|
||||
devEvts = new GBDeviceEvent[]{null};
|
||||
break;
|
||||
case APPLICATIONMESSAGE_NACK:
|
||||
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") NACK");
|
||||
devEvts = new GBDeviceEvent[]{null};
|
||||
if (pebbleCmd == APPLICATIONMESSAGE_ACK) {
|
||||
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") ACK");
|
||||
} else {
|
||||
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") NACK");
|
||||
}
|
||||
GBDeviceEventAppMessage evtAppMessage = null;
|
||||
if (endpoint == ENDPOINT_APPLICATIONMESSAGE && idLookup[last_id] != null) {
|
||||
evtAppMessage = new GBDeviceEventAppMessage();
|
||||
if (pebbleCmd == APPLICATIONMESSAGE_ACK) {
|
||||
evtAppMessage.type = GBDeviceEventAppMessage.TYPE_ACK;
|
||||
} else {
|
||||
evtAppMessage.type = GBDeviceEventAppMessage.TYPE_NACK;
|
||||
}
|
||||
evtAppMessage.id = idLookup[last_id];
|
||||
evtAppMessage.appUUID = currentRunningApp;
|
||||
}
|
||||
devEvts = new GBDeviceEvent[]{evtAppMessage};
|
||||
break;
|
||||
case APPLICATIONMESSAGE_REQUEST:
|
||||
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") REQUEST");
|
||||
|
@ -95,7 +95,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
public void onAppConfiguration(UUID uuid, String config, Integer id) {
|
||||
try {
|
||||
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
|
||||
|
||||
@ -116,7 +116,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
|
||||
}
|
||||
pairs.add(new Pair<>(Integer.parseInt(keyStr), object));
|
||||
}
|
||||
getDeviceIOThread().write(((PebbleProtocol) getDeviceProtocol()).encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, uuid, pairs));
|
||||
getDeviceIOThread().write(((PebbleProtocol) getDeviceProtocol()).encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, uuid, pairs, id));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@ -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,209 @@
|
||||
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 String[] AllowedDomains = new String[]{
|
||||
"openweathermap.org", //for weather :)
|
||||
"tagesschau.de" //for internal watchapp tests
|
||||
};
|
||||
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 && (org.apache.commons.lang3.StringUtils.indexOfAny(requestedUri.getHost(), AllowedDomains) != -1)) {
|
||||
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) {
|
||||
LOG.warn("Error downloading data from " + requestedUri, e);
|
||||
}
|
||||
|
||||
} 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("Error building the JSON weather message.", e);
|
||||
}
|
||||
|
||||
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("Error building the appmessage JSON object", e);
|
||||
}
|
||||
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("Error building the ActiveWathcInfo JSON object", e);
|
||||
}
|
||||
//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("Error loading config file", e);
|
||||
}
|
||||
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("Error reading presets", e);
|
||||
}
|
||||
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("Error storing presets", e);
|
||||
}
|
||||
}
|
||||
|
||||
@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("Error definining local storage prefix", e);
|
||||
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);
|
||||
}
|
||||
}
|
@ -196,7 +196,7 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config) {
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
|
@ -280,4 +280,5 @@ public class DeviceHelper {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,289 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
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.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.SparseArray;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
|
||||
|
||||
public class WebViewSingleton {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebViewSingleton.class);
|
||||
|
||||
private WebView instance = null;
|
||||
private MutableContextWrapper contextWrapper;
|
||||
private Looper mainLooper;
|
||||
private static WebViewSingleton webViewSingleton = new WebViewSingleton();
|
||||
private static UUID currentRunningUUID;
|
||||
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() {
|
||||
}
|
||||
|
||||
public static synchronized void ensureCreated(Activity context) {
|
||||
if (webViewSingleton.instance == null) {
|
||||
webViewSingleton.contextWrapper = new MutableContextWrapper(context);
|
||||
webViewSingleton.mainLooper = context.getMainLooper();
|
||||
webViewSingleton.instance = new WebView(webViewSingleton.contextWrapper);
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
webViewSingleton.instance.setWillNotDraw(true);
|
||||
webViewSingleton.instance.clearCache(true);
|
||||
webViewSingleton.instance.setWebViewClient(new GBWebClient());
|
||||
webViewSingleton.instance.setWebChromeClient(new GBChromeClient());
|
||||
WebSettings webSettings = webViewSingleton.instance.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
//needed to access the DOM
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
//needed for localstorage
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
//Internet helper outgoing connection
|
||||
private static ServiceConnection internetHelperConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
LOG.info("internet helper service bound");
|
||||
internetHelperBound = true;
|
||||
internetHelper = new Messenger(service);
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
LOG.info("internet helper service unbound");
|
||||
internetHelper = null;
|
||||
internetHelperBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
//Internet helper inbound (responses) handler
|
||||
private static class IncomingHandler extends Handler {
|
||||
|
||||
private String getCharsetFromHeaders(String contentType) {
|
||||
if (contentType != null && contentType.toLowerCase().trim().contains("charset=")) {
|
||||
String[] parts = contentType.toLowerCase().trim().split("=");
|
||||
if (parts.length > 0)
|
||||
return parts[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Bundle data = msg.getData();
|
||||
LOG.debug("WEBVIEW: internet helper returned: " + data.getString("response"));
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), 200, "OK",
|
||||
headers,
|
||||
new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type")))))
|
||||
);
|
||||
} else {
|
||||
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type"))))));
|
||||
}
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static WebView getWebView(Context context) {
|
||||
webViewSingleton.contextWrapper.setBaseContext(context);
|
||||
return webViewSingleton.instance;
|
||||
}
|
||||
|
||||
public static void runJavascriptInterface(GBDevice device, UUID uuid) {
|
||||
if (uuid == null && device == null) {
|
||||
throw new RuntimeException("Javascript interface started without device and uuid");
|
||||
}
|
||||
if (uuid.equals(currentRunningUUID)) {
|
||||
LOG.debug("WEBVIEW uuid not changed keeping the old context");
|
||||
} else {
|
||||
final JSInterface jsInterface = new JSInterface(device, uuid);
|
||||
LOG.debug("WEBVIEW uuid changed, restarting");
|
||||
currentRunningUUID = uuid;
|
||||
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
webViewSingleton.instance.onResume();
|
||||
webViewSingleton.instance.removeJavascriptInterface("GBjs");
|
||||
webViewSingleton.instance.addJavascriptInterface(jsInterface, "GBjs");
|
||||
webViewSingleton.instance.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
|
||||
}
|
||||
});
|
||||
if (!internetHelperBound) {
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(new ComponentName("nodomain.freeyourgadget.internethelper", "nodomain.freeyourgadget.internethelper.MyService"));
|
||||
webViewSingleton.contextWrapper.getApplicationContext().bindService(intent, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void appMessage(GBDeviceEventAppMessage message) {
|
||||
|
||||
final String jsEvent;
|
||||
if (webViewSingleton.instance == null) {
|
||||
LOG.warn("WEBVIEW is not initialized, cannot send appMessages to it");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.appUUID.equals(currentRunningUUID)) {
|
||||
LOG.info("WEBVIEW ignoring message for app that is not currently running: " + message.appUUID + " message: " + message.message + " type: " + message.type);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: handle ACK and NACK types with ids
|
||||
if (message.type != GBDeviceEventAppMessage.TYPE_APPMESSAGE) {
|
||||
jsEvent = (GBDeviceEventAppMessage.TYPE_NACK == GBDeviceEventAppMessage.TYPE_APPMESSAGE) ? "NACK" + message.id : "ACK" + message.id;
|
||||
LOG.debug("WEBVIEW received ACK/NACK:" + message.message + " for uuid: " + message.appUUID + " ID: " + message.id);
|
||||
} else {
|
||||
jsEvent = "appmessage";
|
||||
}
|
||||
|
||||
final String appMessage = parseIncomingAppMessage(message.message, message.appUUID);
|
||||
LOG.debug("to WEBVIEW: event: " + jsEvent + " message: " + appMessage);
|
||||
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
webViewSingleton.instance.evaluateJavascript("Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", new ValueCallback<String>() {
|
||||
@Override
|
||||
public void onReceiveValue(String s) {
|
||||
//TODO: the message should be acked here instead of in PebbleIoThread
|
||||
LOG.debug("Callback from appmessage: " + s);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void disposeWebView() {
|
||||
if (internetHelperBound) {
|
||||
LOG.debug("WEBVIEW: will unbind the internet helper");
|
||||
webViewSingleton.contextWrapper.getApplicationContext().unbindService(internetHelperConnection);
|
||||
internetHelperBound = false;
|
||||
}
|
||||
currentRunningUUID = null;
|
||||
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (webViewSingleton.instance != null) {
|
||||
webViewSingleton.instance.removeJavascriptInterface("GBjs");
|
||||
// webViewSingleton.instance.setWebChromeClient(null);
|
||||
// webViewSingleton.instance.setWebViewClient(null);
|
||||
webViewSingleton.instance.clearHistory();
|
||||
webViewSingleton.instance.clearCache(true);
|
||||
webViewSingleton.instance.loadUrl("about:blank");
|
||||
// webViewSingleton.instance.freeMemory();
|
||||
webViewSingleton.instance.pauseTimers();
|
||||
// instance.destroy();
|
||||
// instance = null;
|
||||
// contextWrapper = null;
|
||||
// jsInterface = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static JSONObject getAppConfigurationKeys(UUID uuid) {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, uuid.toString() + ".json");
|
||||
if (configurationFile.exists()) {
|
||||
String jsonString = FileUtils.getStringFromFile(configurationFile);
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
return json.getJSONObject("appKeys");
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
LOG.warn("Unable to parse configuration JSON file", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String parseIncomingAppMessage(String msg, UUID uuid) {
|
||||
JSONObject jsAppMessage = new JSONObject();
|
||||
|
||||
JSONObject knownKeys = getAppConfigurationKeys(uuid);
|
||||
SparseArray<String> appKeysMap = new SparseArray<>();
|
||||
|
||||
if (knownKeys == null || msg == null) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
String inKey, outKey;
|
||||
//knownKeys contains "name"->"index", we need to reverse that
|
||||
for (Iterator<String> key = knownKeys.keys(); key.hasNext(); ) {
|
||||
inKey = key.next();
|
||||
appKeysMap.put(knownKeys.optInt(inKey), inKey);
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray incoming = new JSONArray(msg);
|
||||
JSONObject outgoing = new JSONObject();
|
||||
for (int i = 0; i < incoming.length(); i++) {
|
||||
JSONObject in = incoming.getJSONObject(i);
|
||||
outKey = null;
|
||||
Object outValue = null;
|
||||
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
|
||||
inKey = key.next();
|
||||
switch (inKey) {
|
||||
case "key":
|
||||
outKey = appKeysMap.get(in.optInt(inKey));
|
||||
break;
|
||||
case "value":
|
||||
outValue = in.get(inKey);
|
||||
}
|
||||
}
|
||||
if (outKey != null && outValue != null) {
|
||||
outgoing.put(outKey, outValue);
|
||||
}
|
||||
}
|
||||
jsAppMessage.put("payload", outgoing);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Unable to parse incoming app message", e);
|
||||
}
|
||||
return jsAppMessage.toString();
|
||||
}
|
||||
|
||||
}
|
@ -20,9 +20,14 @@ import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
|
||||
public class ParcelableWeather2 implements Parcelable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ParcelableWeather2.class);
|
||||
|
||||
@ -35,15 +40,17 @@ public class ParcelableWeather2 implements Parcelable {
|
||||
public int currentTemp = 0;
|
||||
public String currentCondition = "";
|
||||
|
||||
String[] currentConditionType = null;
|
||||
private String[] currentConditionType = null;
|
||||
public int currentConditionCode = 3200;
|
||||
String[] forecastConditionType = null;
|
||||
private String[] forecastConditionType = null;
|
||||
public int forecastConditionCode = 3200;
|
||||
public int todayLowTemp = 0;
|
||||
public int todayHighTemp = 0;
|
||||
public int forecastLowTemp = 0;
|
||||
public int forecastHighTemp = 0;
|
||||
|
||||
public JSONObject reconstructedWeather = null;
|
||||
public JSONObject reconstructedForecast = null;
|
||||
|
||||
private ParcelableWeather2(Parcel in) {
|
||||
int version = in.readInt();
|
||||
@ -59,6 +66,11 @@ public class ParcelableWeather2 implements Parcelable {
|
||||
int conditions = bundle.getInt("weather_conditions");
|
||||
if (conditions > 0) {
|
||||
Bundle conditionBundle = in.readBundle();
|
||||
reconstructedWeather = new JSONObject();
|
||||
JSONArray weather = new JSONArray();
|
||||
JSONObject condition = new JSONObject();
|
||||
JSONObject main = new JSONObject();
|
||||
|
||||
currentCondition = conditionBundle.getString("weather_condition_text");
|
||||
conditionBundle.getStringArray("weather_condition_types");
|
||||
currentTemp = conditionBundle.getInt("weather_current_temp");
|
||||
@ -67,22 +79,105 @@ public class ParcelableWeather2 implements Parcelable {
|
||||
currentConditionCode = weatherConditionTypesToOpenWeatherMapIds(currentConditionType[0]);
|
||||
todayLowTemp = conditionBundle.getInt("weather_low_temp");
|
||||
todayHighTemp = conditionBundle.getInt("weather_high_temp");
|
||||
try {
|
||||
condition.put("id", currentConditionCode);
|
||||
condition.put("main", currentCondition);
|
||||
condition.put("icon", Weather.mapToOpenWeatherMapIcon(currentConditionCode));
|
||||
weather.put(condition);
|
||||
|
||||
main.put("temp", currentTemp);
|
||||
main.put("humidity", conditionBundle.getInt("weather_humidity_value"));
|
||||
main.put("temp_min", todayLowTemp);
|
||||
main.put("temp_max", todayHighTemp);
|
||||
main.put("name", location);
|
||||
|
||||
reconstructedWeather.put("weather", weather);
|
||||
reconstructedWeather.put("main", main);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
LOG.debug("Weather JSON for WEBVIEW: " + reconstructedWeather.toString());
|
||||
//fetch immediate next forecast
|
||||
if (--conditions > 0) {
|
||||
int timeOffset = 86400000; //manually determined
|
||||
reconstructedForecast = new JSONObject();
|
||||
JSONArray list = new JSONArray();
|
||||
JSONObject city = new JSONObject();
|
||||
JSONObject item = new JSONObject();
|
||||
condition = new JSONObject();
|
||||
main = new JSONObject();
|
||||
weather = new JSONArray();
|
||||
Bundle forecastBundle = in.readBundle();
|
||||
forecastConditionType = forecastBundle.getStringArray("weather_condition_types");
|
||||
forecastConditionCode = weatherConditionTypesToOpenWeatherMapIds(forecastConditionType[0]);
|
||||
forecastLowTemp = forecastBundle.getInt("weather_low_temp");
|
||||
forecastHighTemp = forecastBundle.getInt("weather_high_temp");
|
||||
try {
|
||||
condition.put("id", forecastConditionCode);
|
||||
condition.put("main", forecastBundle.getString("weather_condition_text"));
|
||||
condition.put("icon", Weather.mapToOpenWeatherMapIcon(forecastConditionCode));
|
||||
weather.put(condition);
|
||||
|
||||
main.put("temp", forecastBundle.getInt("weather_current_temp"));
|
||||
main.put("humidity", forecastBundle.getInt("weather_humidity_value"));
|
||||
main.put("temp_min", forecastLowTemp);
|
||||
main.put("temp_max", forecastHighTemp);
|
||||
|
||||
//forecast
|
||||
//"city":{"id":3181913,"name":"Bolzano","coord":{"lat":46.4927,"lon":11.3336},"country":"IT"}
|
||||
city.put("name", location);
|
||||
city.put("country", "World");
|
||||
reconstructedForecast.put("city", city);
|
||||
|
||||
item.put("dt", (time / 1000) + timeOffset);
|
||||
item.put("main", main);
|
||||
item.put("weather", weather);
|
||||
list.put(item);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// get the rest
|
||||
while (--conditions > 0) {
|
||||
conditionBundle = in.readBundle();
|
||||
conditionBundle.getString("weather_condition_text");
|
||||
weatherConditionTypesToOpenWeatherMapIds(conditionBundle.getStringArray("weather_condition_types")[0]);
|
||||
conditionBundle.getInt("weather_current_temp");
|
||||
item = new JSONObject();
|
||||
condition = new JSONObject();
|
||||
main = new JSONObject();
|
||||
weather = new JSONArray();
|
||||
timeOffset += 86400000;
|
||||
try {
|
||||
condition.put("id", weatherConditionTypesToOpenWeatherMapIds(conditionBundle.getStringArray("weather_condition_types")[0]));
|
||||
condition.put("main", conditionBundle.getString("weather_condition_text"));
|
||||
condition.put("icon", Weather.mapToOpenWeatherMapIcon(weatherConditionTypesToOpenWeatherMapIds(conditionBundle.getStringArray("weather_condition_types")[0])));
|
||||
weather.put(condition);
|
||||
|
||||
main.put("temp", conditionBundle.getInt("weather_current_temp"));
|
||||
main.put("humidity", conditionBundle.getInt("weather_humidity_value"));
|
||||
main.put("temp_min", conditionBundle.getInt("weather_low_temp"));
|
||||
main.put("temp_max", conditionBundle.getInt("weather_high_temp"));
|
||||
|
||||
item.put("dt", (time / 1000) + timeOffset);
|
||||
item.put("main", main);
|
||||
item.put("weather", weather);
|
||||
list.put(item);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
try {
|
||||
reconstructedForecast.put("cnt", list.length());
|
||||
reconstructedForecast.put("list", list);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
LOG.debug("Forecast JSON for WEBVIEW: " + reconstructedForecast.toString());
|
||||
}
|
||||
}
|
||||
// get the rest
|
||||
while (--conditions > 0) {
|
||||
Bundle conditionBundle = in.readBundle();
|
||||
conditionBundle.getString("weather_condition_text");
|
||||
conditionBundle.getStringArray("weather_condition_types");
|
||||
conditionBundle.getInt("weather_current_temp");
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator<ParcelableWeather2> CREATOR = new Creator<ParcelableWeather2>() {
|
||||
|
@ -1,5 +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" />
|
||||
<FrameLayout android:id="@+id/webview_placeholder"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
|
@ -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>
|
||||
|
||||
@ -449,7 +451,11 @@
|
||||
|
||||
<string name="timeformat_24h">24H</string>
|
||||
<string name="timeformat_am_pm">AM/PM</string>
|
||||
|
||||
<string name="pref_screen_notification_profile_alarm_clock">Alarm clock</string>
|
||||
|
||||
<string name="activity_web_view">Web View Activity</string>
|
||||
|
||||
<string name="StringUtils_sender"> (%1$s)</string>
|
||||
<string name="find_device_you_found_it">You found it!</string>
|
||||
<string name="miband2_prefs_timeformat">Mi2: Time format</string>
|
||||
|
@ -419,6 +419,12 @@
|
||||
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:dependency="pebble_force_untested"
|
||||
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"
|
||||
|
@ -105,7 +105,7 @@ class TestDeviceSupport extends AbstractDeviceSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config) {
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user