From 089a59168ec0b9757c8a41d12a1087fa1cc1f0ea Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Sun, 28 Feb 2016 22:25:21 +0100 Subject: [PATCH] Initial support for using an external browser for configuring pebble apps. This allows existing configuration pages to work without having internet access ourselves. This is a better approach as initially thought in #191. What is missing is outlined in the (several) TODOs. --- app/src/main/AndroidManifest.xml | 16 + app/src/main/assets/app_config/configure.html | 25 + app/src/main/assets/app_config/js/Uri.js | 458 ++++++++++++++++++ .../app_config/js/gadgetbridge_boilerplate.js | 95 ++++ .../activities/AppManagerActivity.java | 5 + .../activities/ExternalPebbleJSActivity.java | 153 ++++++ .../devices/pebble/PBWInstallHandler.java | 18 + .../devices/pebble/PBWReader.java | 19 +- .../layout/activity_external_pebble_js.xml | 5 + app/src/main/res/menu/appmanager_context.xml | 4 +- app/src/main/res/values/strings.xml | 1 + 11 files changed, 797 insertions(+), 2 deletions(-) create mode 100644 app/src/main/assets/app_config/configure.html create mode 100644 app/src/main/assets/app_config/js/Uri.js create mode 100644 app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java create mode 100644 app/src/main/res/layout/activity_external_pebble_js.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49860022..b9b8c99c 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 00000000..ac25d82c --- /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 00000000..a3fe4998 --- /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 00000000..af68a965 --- /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 d87f7241..0ff6333a 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 00000000..cb79cc7a --- /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 4071c83d..243b5deb 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 e463dd44..ee65b567 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 00000000..551fb927 --- /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 a139eb1a..dd5d22c3 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 8ab85988..fc9e2a29 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