diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 49860022e..b9b8c99cb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -257,6 +257,22 @@
android:name="android.appwidget.provider"
android:resource="@xml/sleep_alarm_widget_info" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/app_config/configure.html b/app/src/main/assets/app_config/configure.html
new file mode 100644
index 000000000..ac25d82ce
--- /dev/null
+++ b/app/src/main/assets/app_config/configure.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
Url of the configuration:
+
+
+
+
+
Incoming configuration data:
+
+
+
+
diff --git a/app/src/main/assets/app_config/js/Uri.js b/app/src/main/assets/app_config/js/Uri.js
new file mode 100644
index 000000000..a3fe49982
--- /dev/null
+++ b/app/src/main/assets/app_config/js/Uri.js
@@ -0,0 +1,458 @@
+/*!
+ * jsUri
+ * https://github.com/derek-watson/jsUri
+ *
+ * Copyright 2013, Derek Watson
+ * Released under the MIT license.
+ *
+ * Includes parseUri regular expressions
+ * http://blog.stevenlevithan.com/archives/parseuri
+ * Copyright 2007, Steven Levithan
+ * Released under the MIT license.
+ */
+
+ /*globals define, module */
+
+(function(global) {
+
+ var re = {
+ starts_with_slashes: /^\/+/,
+ ends_with_slashes: /\/+$/,
+ pluses: /\+/g,
+ query_separator: /[&;]/,
+ uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@\/]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+ };
+
+ /**
+ * Define forEach for older js environments
+ * @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach#Compatibility
+ */
+ if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(callback, thisArg) {
+ var T, k;
+
+ if (this == null) {
+ throw new TypeError(' this is null or not defined');
+ }
+
+ var O = Object(this);
+ var len = O.length >>> 0;
+
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + ' is not a function');
+ }
+
+ if (arguments.length > 1) {
+ T = thisArg;
+ }
+
+ k = 0;
+
+ while (k < len) {
+ var kValue;
+ if (k in O) {
+ kValue = O[k];
+ callback.call(T, kValue, k, O);
+ }
+ k++;
+ }
+ };
+ }
+
+ /**
+ * unescape a query param value
+ * @param {string} s encoded value
+ * @return {string} decoded value
+ */
+ function decode(s) {
+ if (s) {
+ s = s.toString().replace(re.pluses, '%20');
+ s = decodeURIComponent(s);
+ }
+ return s;
+ }
+
+ /**
+ * Breaks a uri string down into its individual parts
+ * @param {string} str uri
+ * @return {object} parts
+ */
+ function parseUri(str) {
+ var parser = re.uri_parser;
+ var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
+ var m = parser.exec(str || '');
+ var parts = {};
+
+ parserKeys.forEach(function(key, i) {
+ parts[key] = m[i] || '';
+ });
+
+ return parts;
+ }
+
+ /**
+ * Breaks a query string down into an array of key/value pairs
+ * @param {string} str query
+ * @return {array} array of arrays (key/value pairs)
+ */
+ function parseQuery(str) {
+ var i, ps, p, n, k, v, l;
+ var pairs = [];
+
+ if (typeof(str) === 'undefined' || str === null || str === '') {
+ return pairs;
+ }
+
+ if (str.indexOf('?') === 0) {
+ str = str.substring(1);
+ }
+
+ ps = str.toString().split(re.query_separator);
+
+ for (i = 0, l = ps.length; i < l; i++) {
+ p = ps[i];
+ n = p.indexOf('=');
+
+ if (n !== 0) {
+ k = decode(p.substring(0, n));
+ v = decode(p.substring(n + 1));
+ pairs.push(n === -1 ? [p, null] : [k, v]);
+ }
+
+ }
+ return pairs;
+ }
+
+ /**
+ * Creates a new Uri object
+ * @constructor
+ * @param {string} str
+ */
+ function Uri(str) {
+ this.uriParts = parseUri(str);
+ this.queryPairs = parseQuery(this.uriParts.query);
+ this.hasAuthorityPrefixUserPref = null;
+ }
+
+ /**
+ * Define getter/setter methods
+ */
+ ['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function(key) {
+ Uri.prototype[key] = function(val) {
+ if (typeof val !== 'undefined') {
+ this.uriParts[key] = val;
+ }
+ return this.uriParts[key];
+ };
+ });
+
+ /**
+ * if there is no protocol, the leading // can be enabled or disabled
+ * @param {Boolean} val
+ * @return {Boolean}
+ */
+ Uri.prototype.hasAuthorityPrefix = function(val) {
+ if (typeof val !== 'undefined') {
+ this.hasAuthorityPrefixUserPref = val;
+ }
+
+ if (this.hasAuthorityPrefixUserPref === null) {
+ return (this.uriParts.source.indexOf('//') !== -1);
+ } else {
+ return this.hasAuthorityPrefixUserPref;
+ }
+ };
+
+ Uri.prototype.isColonUri = function (val) {
+ if (typeof val !== 'undefined') {
+ this.uriParts.isColonUri = !!val;
+ } else {
+ return !!this.uriParts.isColonUri;
+ }
+ };
+
+ /**
+ * Serializes the internal state of the query pairs
+ * @param {string} [val] set a new query string
+ * @return {string} query string
+ */
+ Uri.prototype.query = function(val) {
+ var s = '', i, param, l;
+
+ if (typeof val !== 'undefined') {
+ this.queryPairs = parseQuery(val);
+ }
+
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (s.length > 0) {
+ s += '&';
+ }
+ if (param[1] === null) {
+ s += param[0];
+ } else {
+ s += param[0];
+ s += '=';
+ if (typeof param[1] !== 'undefined') {
+ s += encodeURIComponent(param[1]);
+ }
+ }
+ }
+ return s.length > 0 ? '?' + s : s;
+ };
+
+ /**
+ * returns the first query param value found for the key
+ * @param {string} key query key
+ * @return {string} first value found for key
+ */
+ Uri.prototype.getQueryParamValue = function (key) {
+ var param, i, l;
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (key === param[0]) {
+ return param[1];
+ }
+ }
+ };
+
+ /**
+ * returns an array of query param values for the key
+ * @param {string} key query key
+ * @return {array} array of values
+ */
+ Uri.prototype.getQueryParamValues = function (key) {
+ var arr = [], i, param, l;
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (key === param[0]) {
+ arr.push(param[1]);
+ }
+ }
+ return arr;
+ };
+
+ /**
+ * removes query parameters
+ * @param {string} key remove values for key
+ * @param {val} [val] remove a specific value, otherwise removes all
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.deleteQueryParam = function (key, val) {
+ var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;
+
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+
+ param = this.queryPairs[i];
+ keyMatchesFilter = decode(param[0]) === decode(key);
+ valMatchesFilter = param[1] === val;
+
+ if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
+ arr.push(param);
+ }
+ }
+
+ this.queryPairs = arr;
+
+ return this;
+ };
+
+ /**
+ * adds a query parameter
+ * @param {string} key add values for key
+ * @param {string} val value to add
+ * @param {integer} [index] specific index to add the value at
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.addQueryParam = function (key, val, index) {
+ if (arguments.length === 3 && index !== -1) {
+ index = Math.min(index, this.queryPairs.length);
+ this.queryPairs.splice(index, 0, [key, val]);
+ } else if (arguments.length > 0) {
+ this.queryPairs.push([key, val]);
+ }
+ return this;
+ };
+
+ /**
+ * test for the existence of a query parameter
+ * @param {string} key check values for key
+ * @return {Boolean} true if key exists, otherwise false
+ */
+ Uri.prototype.hasQueryParam = function (key) {
+ var i, len = this.queryPairs.length;
+ for (i = 0; i < len; i++) {
+ if (this.queryPairs[i][0] == key)
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * replaces query param values
+ * @param {string} key key to replace value for
+ * @param {string} newVal new value
+ * @param {string} [oldVal] replace only one specific value (otherwise replaces all)
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
+ var index = -1, len = this.queryPairs.length, i, param;
+
+ if (arguments.length === 3) {
+ for (i = 0; i < len; i++) {
+ param = this.queryPairs[i];
+ if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
+ index = i;
+ break;
+ }
+ }
+ if (index >= 0) {
+ this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index);
+ }
+ } else {
+ for (i = 0; i < len; i++) {
+ param = this.queryPairs[i];
+ if (decode(param[0]) === decode(key)) {
+ index = i;
+ break;
+ }
+ }
+ this.deleteQueryParam(key);
+ this.addQueryParam(key, newVal, index);
+ }
+ return this;
+ };
+
+ /**
+ * Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc)
+ */
+ ['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function(key) {
+ var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
+ Uri.prototype[method] = function(val) {
+ this[key](val);
+ return this;
+ };
+ });
+
+ /**
+ * Scheme name, colon and doubleslash, as required
+ * @return {string} http:// or possibly just //
+ */
+ Uri.prototype.scheme = function() {
+ var s = '';
+
+ if (this.protocol()) {
+ s += this.protocol();
+ if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
+ s += ':';
+ }
+ s += '//';
+ } else {
+ if (this.hasAuthorityPrefix() && this.host()) {
+ s += '//';
+ }
+ }
+
+ return s;
+ };
+
+ /**
+ * Same as Mozilla nsIURI.prePath
+ * @return {string} scheme://user:password@host:port
+ * @see https://developer.mozilla.org/en/nsIURI
+ */
+ Uri.prototype.origin = function() {
+ var s = this.scheme();
+
+ if (this.userInfo() && this.host()) {
+ s += this.userInfo();
+ if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
+ s += '@';
+ }
+ }
+
+ if (this.host()) {
+ s += this.host();
+ if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) {
+ s += ':' + this.port();
+ }
+ }
+
+ return s;
+ };
+
+ /**
+ * Adds a trailing slash to the path
+ */
+ Uri.prototype.addTrailingSlash = function() {
+ var path = this.path() || '';
+
+ if (path.substr(-1) !== '/') {
+ this.path(path + '/');
+ }
+
+ return this;
+ };
+
+ /**
+ * Serializes the internal state of the Uri object
+ * @return {string}
+ */
+ Uri.prototype.toString = function() {
+ var path, s = this.origin();
+
+ if (this.isColonUri()) {
+ if (this.path()) {
+ s += ':'+this.path();
+ }
+ } else if (this.path()) {
+ path = this.path();
+ if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
+ s += '/';
+ } else {
+ if (s) {
+ s.replace(re.ends_with_slashes, '/');
+ }
+ path = path.replace(re.starts_with_slashes, '/');
+ }
+ s += path;
+ } else {
+ if (this.host() && (this.query().toString() || this.anchor())) {
+ s += '/';
+ }
+ }
+ if (this.query().toString()) {
+ s += this.query().toString();
+ }
+
+ if (this.anchor()) {
+ if (this.anchor().indexOf('#') !== 0) {
+ s += '#';
+ }
+ s += this.anchor();
+ }
+
+ return s;
+ };
+
+ /**
+ * Clone a Uri object
+ * @return {Uri} duplicate copy of the Uri
+ */
+ Uri.prototype.clone = function() {
+ return new Uri(this.toString());
+ };
+
+ /**
+ * export via AMD or CommonJS, otherwise leak a global
+ */
+ if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return Uri;
+ });
+ } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ module.exports = Uri;
+ } else {
+ global.Uri = Uri;
+ }
+}(this));
diff --git a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
new file mode 100644
index 000000000..af68a9653
--- /dev/null
+++ b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
@@ -0,0 +1,95 @@
+function loadScript(url, callback) {
+ // Adding the script tag to the head as suggested before
+ var head = document.getElementsByTagName('head')[0];
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = url;
+
+ // Then bind the event to the callback function.
+ // There are several events for cross browser compatibility.
+ script.onreadystatechange = callback;
+ script.onload = callback;
+
+ // Fire the loading
+ head.appendChild(script);
+}
+
+function getURLVariable(variable, defaultValue) {
+ // Find all URL parameters
+ var query = location.search.substring(1);
+ var vars = query.split('&');
+ for (var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split('=');
+
+ // If the query variable parameter is found, decode it to use and return it for use
+ if (pair[0] === variable) {
+ return decodeURIComponent(pair[1]);
+ }
+ }
+ return defaultValue || false;
+}
+
+function gbPebble() {
+ this.configurationURL = null;
+ this.configurationValues = null;
+
+ this.addEventListener = function(e, f) {
+ if(e == 'showConfiguration') {
+ this.showConfiguration = f;
+ }
+ if(e == 'webviewclosed') {
+ this.parseconfig = f;
+ }
+ if(e == 'appmessage') {
+ this.appmessage = f;
+ }
+ }
+
+ this.actuallyOpenURL = function() {
+ window.open(this.configurationURL.toString(), "config");
+ }
+
+ this.actuallySendData = function() {
+ GBjs.sendAppMessage(this.configurationValues);
+ }
+
+ //needs to be called like this because of original Pebble function name
+ this.openURL = function(url) {
+ document.getElementById("config_url").innerHTML=url;
+ var UUID = GBjs.getAppUUID();
+ this.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
+ }
+
+ this.getActiveWatchInfo = function() {
+ return JSON.parse(GBjs.getActiveWatchInfo());
+ }
+
+ this.sendAppMessage = function (dict, callback){
+ this.configurationValues = JSON.stringify(dict);
+ document.getElementById("jsondata").innerHTML=this.configurationValues;
+ return callback;
+ }
+
+ this.ready = function(e) {
+ GBjs.gbLog("ready called");
+ }
+}
+
+var Pebble = new gbPebble();
+
+var jsConfigFile = GBjs.getAppConfigurationFile();
+if (jsConfigFile != null) {
+ loadScript(jsConfigFile, function() {
+ if (getURLVariable('config') == 'true') {
+ document.getElementById('step1').style.display="none";
+ var json_string = unescape(getURLVariable('json'));
+ var t = new Object();
+ t.response = json_string;
+ if (json_string != '')
+ Pebble.parseconfig(t);
+ } else {
+ document.getElementById('step2').style.display="none";
+ Pebble.showConfiguration();
+ }
+ });
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
index d87f7241a..0ff6333aa 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
@@ -189,6 +189,11 @@ public class AppManagerActivity extends Activity {
case R.id.appmanager_health_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
return true;
+ case R.id.appmanager_app_configure:
+ Intent startIntent = new Intent(getApplicationContext(), ExternalPebbleJSActivity.class);
+ startIntent.putExtra("app_uuid", selectedApp.getUUID());
+ startActivity(startIntent);
+ return true;
default:
return super.onContextItemSelected(item);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
new file mode 100644
index 000000000..cb79cc7a5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
@@ -0,0 +1,153 @@
+package nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class ExternalPebbleJSActivity extends Activity {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
+
+ //TODO: get device
+ private Uri uri;
+ private UUID appUuid;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String queryString = "";
+ uri = getIntent().getData();
+ if (uri != null) {
+ //getting back with configuration data
+ appUuid = UUID.fromString(uri.getHost());
+ queryString = uri.getEncodedQuery();
+ } else {
+ appUuid = (UUID) getIntent().getSerializableExtra("app_uuid");
+ }
+
+ setContentView(R.layout.activity_external_pebble_js);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ WebView myWebView = (WebView) findViewById(R.id.configureWebview);
+ myWebView.clearCache(true);
+ WebSettings webSettings = myWebView.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ //needed to access the DOM
+ webSettings.setDomStorageEnabled(true);
+
+ JSInterface gbJSInterface = new JSInterface();
+ myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
+
+ 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 e) {
+ e.printStackTrace();
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private class JSInterface {
+
+ public JSInterface () {
+ }
+
+ @JavascriptInterface
+ public void gbLog(String msg) {
+ Log.d("WEBVIEW", msg);
+ }
+
+ @JavascriptInterface
+ public void sendAppMessage(String msg) {
+ Log.d("from WEBVIEW", msg);
+ JSONObject knownKeys = getAppConfigurationKeys();
+ ArrayList> pairs = new ArrayList<>();
+
+ try{
+ JSONObject in = new JSONObject(msg);
+ String cur_key;
+ for (Iterator key = in.keys(); key.hasNext();) {
+ cur_key = key.next();
+ int pebbleAppIndex = knownKeys.optInt(cur_key);
+ if (pebbleAppIndex != 0) {
+ //TODO: cast to integer (int32) / String? Is it needed?
+ pairs.add(new Pair<>(pebbleAppIndex, (Object) in.get(cur_key)));
+ } else {
+ GB.toast("Discarded key "+cur_key+", not found in the local configuration.", Toast.LENGTH_SHORT, GB.WARN);
+ }
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ //TODO: send pairs to pebble. (encodeApplicationMessagePush(ENDPOINT_APPLICATIONMESSAGE, uuid, pairs);)
+ }
+
+ @JavascriptInterface
+ public String getActiveWatchInfo() {
+ //TODO: interact with GBDevice, see also todo at the beginning
+ JSONObject wi = new JSONObject();
+ try {
+ wi.put("platform", "basalt");
+ }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 getAppUUID() {
+ return appUuid.toString();
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
index 4071c83df..243b5deb0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
@@ -173,6 +173,24 @@ public class PBWInstallHandler implements InstallHandler {
} catch (JSONException e) {
LOG.error(e.getMessage(), e);
}
+
+ String jsConfigFile = mPBWReader.getJsConfigurationFile();
+
+ if (jsConfigFile != null) {
+ outputFile = new File(destDir, app.getUUID().toString() + "_config.js");
+ try {
+ writer = new BufferedWriter(new FileWriter(outputFile));
+ } catch (IOException e) {
+ LOG.error("Failed to open output file: " + e.getMessage(), e);
+ return;
+ }
+ try {
+ writer.write(jsConfigFile);
+ writer.close();
+ } catch (IOException e) {
+ LOG.error("Failed to write to output file: " + e.getMessage(), e);
+ }
+ }
}
public boolean isValid() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
index e463dd443..ee65b5674 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
@@ -57,6 +57,7 @@ public class PBWReader {
private short mAppVersion;
private int mIconId;
private int mFlags;
+ private String jsConfigurationFile = null;
private JSONObject mAppKeys = null;
@@ -212,6 +213,18 @@ public class PBWReader {
e.printStackTrace();
break;
}
+ } else if (fileName.equals("pebble-js-app.js")) {
+ LOG.info("Found JS file: app supports configuration.");
+ long bytes = ze.getSize();
+ if (bytes > 65536) // that should be too much
+ break;
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ while ((count = zis.read(buffer)) != -1) {
+ baos.write(buffer, 0, count);
+ }
+
+ jsConfigurationFile = baos.toString();
} else if (fileName.equals(platformDir + "pebble-app.bin")) {
zis.read(buffer, 0, 108);
byte[] tmp_buf = new byte[32];
@@ -327,4 +340,8 @@ public class PBWReader {
public JSONObject getAppKeysJSON() {
return mAppKeys;
}
-}
+
+ public String getJsConfigurationFile() {
+ return jsConfigurationFile;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_external_pebble_js.xml b/app/src/main/res/layout/activity_external_pebble_js.xml
new file mode 100644
index 000000000..551fb9276
--- /dev/null
+++ b/app/src/main/res/layout/activity_external_pebble_js.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/app/src/main/res/menu/appmanager_context.xml b/app/src/main/res/menu/appmanager_context.xml
index a139eb1a7..dd5d22c3b 100644
--- a/app/src/main/res/menu/appmanager_context.xml
+++ b/app/src/main/res/menu/appmanager_context.xml
@@ -12,5 +12,7 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8ab859887..fc9e2a29b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -224,6 +224,7 @@
Deactivate
authenticating
authentication required
+ Configure
Zzz
Add widget