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.
This commit is contained in:
parent
652c5575b3
commit
089a59168e
|
@ -257,6 +257,22 @@
|
|||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/sleep_alarm_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ExternalPebbleJSActivity"
|
||||
android:label="external_js"
|
||||
android:parentActivityName=".activities.AppManagerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="gadgetbridge" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0'>
|
||||
<script type="text/javascript" src="js/Uri.js">
|
||||
</script>
|
||||
<script type="text/javascript" src="js/gadgetbridge_boilerplate.js">
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
<style>
|
||||
<!-- TODO -->
|
||||
</style>
|
||||
</head>
|
||||
<body style="width: 100%;">
|
||||
<div id="step1">
|
||||
<h2>Url of the configuration:</h2>
|
||||
<div id="config_url" style="height: 100px; width: 100%;"></div>
|
||||
<button name="open config" value="open config" onclick="Pebble.actuallyOpenURL()" >Open configuration website</button>
|
||||
</div>
|
||||
<div id="step2">
|
||||
<h2>Incoming configuration data:</h2>
|
||||
<div id="jsondata" style="height: 100px; width: 100%;"></div>
|
||||
<button name="send config" value="send config" onclick="Pebble.actuallySendData()" >Send data to pebble</button>
|
||||
</div>
|
||||
</body>
|
|
@ -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));
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<Pair<Integer, Object>> pairs = new ArrayList<>();
|
||||
|
||||
try{
|
||||
JSONObject in = new JSONObject(msg);
|
||||
String cur_key;
|
||||
for (Iterator<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -12,5 +12,7 @@
|
|||
<item
|
||||
android:id="@+id/appmanager_health_deactivate"
|
||||
android:title="@string/appmanager_health_deactivate"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/appmanager_app_configure"
|
||||
android:title="@string/app_configure"/>
|
||||
</menu>
|
|
@ -224,6 +224,7 @@
|
|||
<string name="appmanager_health_deactivate">Deactivate</string>
|
||||
<string name="authenticating">authenticating</string>
|
||||
<string name="authentication_required">authentication required</string>
|
||||
<string name="app_configure">Configure</string>
|
||||
|
||||
<string name="appwidget_text">Zzz</string>
|
||||
<string name="add_widget">Add widget</string>
|
||||
|
|
Loading…
Reference in New Issue