diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49860022e..c04cea7ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,6 +53,7 @@ android:label="@string/preferences_miband_settings" android:parentActivityName=".activities.SettingsActivity" /> @@ -257,6 +258,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..760a2ec83 --- /dev/null +++ b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js @@ -0,0 +1,128 @@ +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 == 'ready') { + this.ready = f; + } + if(e == 'showConfiguration') { + this.showConfiguration = f; + } + if(e == 'webviewclosed') { + this.parseconfig = f; + } + if(e == 'appmessage') { + this.appmessage = f; + } + } + + this.removeEventListener = function(e, f) { + if(e == 'ready') { + this.ready = null; + } + if(e == 'showConfiguration') { + this.showConfiguration = null; + } + if(e == 'webviewclosed') { + this.parseconfig = null; + } + if(e == 'appmessage') { + this.appmessage = null; + } + } + 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, callbackAck, callbackNack){ + try { + this.configurationValues = JSON.stringify(dict); + document.getElementById("jsondata").innerHTML=this.configurationValues; + return callbackAck; + } + catch (e) { + GBjs.gbLog("sendAppMessage failed"); + return callbackNack; + } + } + + this.getAccountToken = function() { + return ''; + } + + this.getWatchToken = function() { + return GBjs.getWatchToken(); + } + + this.showSimpleNotificationOnPebble = function(title, body) { + GBjs.gbLog("app wanted to show: " + title + " body: "+ body); + } + + +} + +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..080dba67d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java @@ -30,6 +30,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; @@ -72,6 +73,7 @@ public class AppManagerActivity extends Activity { private final List appList = new ArrayList<>(); private GBDeviceAppAdapter mGBDeviceAppAdapter; private GBDeviceApp selectedApp = null; + private GBDevice mGBDevice = null; private List getSystemApps() { List systemApps = new ArrayList<>(); @@ -116,6 +118,13 @@ public class AppManagerActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); + } else { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); setContentView(R.layout.activity_appmanager); @@ -189,6 +198,14 @@ 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: + GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true); + + Intent startIntent = new Intent(getApplicationContext(), ExternalPebbleJSActivity.class); + startIntent.putExtra("app_uuid", selectedApp.getUUID()); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); + startActivity(startIntent); + return true; default: return super.onContextItemSelected(item); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java index 408592c25..e4b04a6be 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java @@ -131,7 +131,7 @@ public class ControlCenter extends Activity { @Override public void onItemClick(AdapterView parent, View v, int position, long id) { GBDevice gbDevice = deviceList.get(position); - if (gbDevice.isConnected()) { + if (gbDevice.isInitialized()) { DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); Class primaryActivity = coordinator.getPrimaryActivity(); if (primaryActivity != null) { 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..36e8101e5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java @@ -0,0 +1,183 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.util.Log; +import android.view.MenuItem; +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.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; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; + +public class ExternalPebbleJSActivity extends Activity { + + private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class); + + private UUID appUuid; + private GBDevice mGBDevice = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); + } else { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + + String queryString = ""; + Uri 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 | 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.debug("from WEBVIEW: ", msg); + JSONObject knownKeys = getAppConfigurationKeys(); + + try { + JSONObject in = new JSONObject(msg); + JSONObject out = new JSONObject(); + String cur_key; + for (Iterator key = in.keys(); key.hasNext(); ) { + cur_key = key.next(); + int pebbleAppIndex = knownKeys.optInt(cur_key); + if (pebbleAppIndex != 0) { + Object obj = in.get(cur_key); + if (obj instanceof Boolean) { + obj = ((Boolean) obj) ? "true" : "false"; + } + out.put(String.valueOf(pebbleAppIndex), obj); + } else { + GB.toast("Discarded key " + cur_key + ", not found in the local configuration.", 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.getHardwareVersion())); + wi.put("model", PebbleUtils.getModel(mGBDevice.getHardwareVersion())); + //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 getAppUUID() { + return appUuid.toString(); + } + + @JavascriptInterface + public String getWatchToken() { + //specification says: A string that is 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(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 0d3de3701..adcf077d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -36,6 +36,8 @@ public interface EventHandler { void onAppDelete(UUID uuid); + void onAppConfiguration(UUID appUuid, String config); + void onFetchActivityData(); void onReboot(); @@ -45,4 +47,5 @@ public interface EventHandler { void onFindDevice(boolean start); void onScreenshotReq(); + } 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..9794ca8fa 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 @@ -23,6 +23,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; public class PBWInstallHandler implements InstallHandler { private static final Logger LOG = LoggerFactory.getLogger(PBWInstallHandler.class); @@ -49,15 +50,7 @@ public class PBWInstallHandler implements InstallHandler { return; } - String hwRev = device.getHardwareVersion(); - String platformName; - if (hwRev.startsWith("snowy")) { - platformName = "basalt"; - } else if (hwRev.startsWith("spalding")) { - platformName = "chalk"; - } else { - platformName = "aplite"; - } + String platformName = PebbleUtils.getPlatformName(device.getHardwareVersion()); try { mPBWReader = new PBWReader(mUri, mContext, platformName); @@ -173,6 +166,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..e287b3288 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,20 @@ 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) { + LOG.info("size exceeding 64k, skipping"); + continue; + } + + 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 +342,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/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 027a54c3d..222786937 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -159,6 +159,14 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + Intent intent = createIntent().setAction(ACTION_APP_CONFIGURE) + .putExtra(EXTRA_APP_UUID, uuid) + .putExtra(EXTRA_APP_CONFIG, config); + invokeService(intent); + } + @Override public void onFetchActivityData() { Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java index cb2b1f489..c0a436a68 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java @@ -64,7 +64,7 @@ public class ActivityUser { * value is out of any logical bounds. */ public int getActivityUserSleepDuration() { - if(activityUserSleepDuration == null) { + if (activityUserSleepDuration == null) { fetchPreferences(); } if (activityUserSleepDuration < 1 || activityUserSleepDuration > 24) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index bd4f33e9d..498cb29c9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -15,7 +15,6 @@ public interface DeviceService extends EventHandler { String ACTION_START = PREFIX + ".action.start"; String ACTION_CONNECT = PREFIX + ".action.connect"; String ACTION_NOTIFICATION = PREFIX + ".action.notification"; - String ACTION_NOTIFICATION_SMS = PREFIX + ".action.notification_sms"; String ACTION_CALLSTATE = PREFIX + ".action.callstate"; String ACTION_SETTIME = PREFIX + ".action.settime"; String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo"; @@ -24,6 +23,7 @@ public interface DeviceService extends EventHandler { String ACTION_REQUEST_SCREENSHOT = PREFIX + ".action.request_screenshot"; String ACTION_STARTAPP = PREFIX + ".action.startapp"; String ACTION_DELETEAPP = PREFIX + ".action.deleteapp"; + String ACTION_APP_CONFIGURE = PREFIX + ".action.app_configure"; String ACTION_INSTALL = PREFIX + ".action.install"; String ACTION_REBOOT = PREFIX + ".action.reboot"; String ACTION_HEARTRATE_TEST = PREFIX + ".action.heartrate_test"; @@ -51,6 +51,7 @@ public interface DeviceService extends EventHandler { String EXTRA_MUSIC_TRACK = "music_track"; String EXTRA_APP_UUID = "app_uuid"; String EXTRA_APP_START = "app_start"; + String EXTRA_APP_CONFIG = "app_config"; String EXTRA_URI = "uri"; String EXTRA_ALARMS = "alarms"; String EXTRA_PERFORM_PAIR = "perform_pair"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index bae6f8b90..f3b8c58b9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP; @@ -60,6 +61,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; 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_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND; @@ -306,6 +308,11 @@ public class DeviceCommunicationService extends Service { mDeviceSupport.onAppDelete(uuid); break; } + case ACTION_APP_CONFIGURE: { + UUID uuid = (UUID) intent.getSerializableExtra(EXTRA_APP_UUID); + String config = intent.getStringExtra(EXTRA_APP_CONFIG); + mDeviceSupport.onAppConfiguration(uuid, config); + } case ACTION_INSTALL: Uri uri = intent.getParcelableExtra(EXTRA_URI); if (uri != null) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 4ddeff3af..d13c90d22 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -178,6 +178,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onAppDelete(uuid); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + if (checkBusy("app configuration")) { + return; + } + delegate.onAppConfiguration(uuid, config); + } + @Override public void onFetchActivityData() { if (checkBusy("fetch activity data")) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java index 20b349ec1..53272f326 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java @@ -656,6 +656,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // not supported } + @Override + public void onAppConfiguration(UUID uuid, String config) { + // not supported + } + @Override public void onScreenshotReq() { // not supported diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java index c29460da9..8b90c5a68 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java @@ -96,18 +96,24 @@ public class AppMessageHandlerPebStyle extends AppMessageHandler { @Override public GBDeviceEvent[] handleMessage(ArrayList> pairs) { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); ByteBuffer buf = ByteBuffer.allocate(encodeAck().length + encodePebStyleConfig().length); buf.put(encodeAck()); buf.put(encodePebStyleConfig()); sendBytes.encodedBytes = buf.array(); return new GBDeviceEvent[]{sendBytes}; + */ } @Override public GBDeviceEvent[] pushMessage() { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); sendBytes.encodedBytes = encodePebStyleConfig(); return new GBDeviceEvent[]{sendBytes}; + */ } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java index 11b8cf78a..fb1583f7e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java @@ -100,8 +100,11 @@ public class AppMessageHandlerTimeStylePebble extends AppMessageHandler { @Override public GBDeviceEvent[] handleMessage(ArrayList> pairs) { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); sendBytes.encodedBytes = encodeTimeStylePebbleConfig(); return new GBDeviceEvent[]{sendBytes}; + */ } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index e3ffaec42..b5bfe222d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; public class PebbleIoThread extends GBDeviceIoThread { private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class); @@ -577,15 +578,7 @@ public class PebbleIoThread extends GBDeviceIoThread { return; } - String hwRev = gbDevice.getHardwareVersion(); - String platformName; - if (hwRev.startsWith("snowy")) { - platformName = "basalt"; - } else if (hwRev.startsWith("spalding")) { - platformName = "chalk"; - } else { - platformName = "aplite"; - } + String platformName = PebbleUtils.getPlatformName(gbDevice.getHardwareVersion()); try { mPBWReader = new PBWReader(uri, getContext(), platformName); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index 0f0b33999..66560c0d0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -1,8 +1,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; import android.net.Uri; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; +import java.util.Iterator; +import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; @@ -37,6 +43,24 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { getDeviceIOThread().installApp(uri, 0); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + try { + ArrayList> pairs = new ArrayList<>(); + + JSONObject json = new JSONObject(config); + Iterator keysIterator = json.keys(); + while (keysIterator.hasNext()) { + String keyStr = keysIterator.next(); + Object object = json.get(keyStr); + pairs.add(new Pair<>(Integer.parseInt(keyStr), object)); + } + getDeviceIOThread().write(((PebbleProtocol) getDeviceProtocol()).encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, uuid, pairs)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + @Override public void onHeartRateTest() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java new file mode 100644 index 000000000..0e13e925d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class PebbleUtils { + public static String getPlatformName(String hwRev) { + String platformName; + if (hwRev.startsWith("snowy")) { + platformName = "basalt"; + } else if (hwRev.startsWith("spalding")) { + platformName = "chalk"; + } else { + platformName = "aplite"; + } + return platformName; + } + public static String getModel(String hwRev) { + //TODO: get real data? + String model; + if (hwRev.startsWith("snowy")) { + model = "pebble_time_black"; + } else if (hwRev.startsWith("spalding")) { + model = "pebble_time_round_black_20mm"; + } else { + model = "pebble_black"; + } + return model; + } +} 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 diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java index 77158b155..8722f00d0 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -90,6 +90,11 @@ public class TestDeviceSupport extends AbstractDeviceSupport { } + @Override + public void onAppConfiguration(UUID appUuid, String config) { + + } + @Override public void onFetchActivityData() {