Use external browser for configuring pebble apps #235

Merged
ashimokawa merged 16 commits from feature-configuration into master 6 years ago
  1. 17
      app/src/main/AndroidManifest.xml
  2. 25
      app/src/main/assets/app_config/configure.html
  3. 458
      app/src/main/assets/app_config/js/Uri.js
  4. 128
      app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js
  5. 17
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java
  6. 2
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java
  7. 183
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
  8. 3
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
  9. 29
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java
  10. 21
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java
  11. 8
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
  12. 2
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java
  13. 3
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
  14. 7
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
  15. 8
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
  16. 5
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java
  17. 6
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java
  18. 3
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java
  19. 11
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java
  20. 24
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java
  21. 27
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java
  22. 5
      app/src/main/res/layout/activity_external_pebble_js.xml
  23. 4
      app/src/main/res/menu/appmanager_context.xml
  24. 1
      app/src/main/res/values/strings.xml
  25. 5
      app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java

17
app/src/main/AndroidManifest.xml

@ -53,6 +53,7 @@
android:label="@string/preferences_miband_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:launchMode="singleTop"
android:name=".activities.AppManagerActivity"
android:label="@string/title_activity_appmanager"
android:parentActivityName=".activities.ControlCenter" />
@ -257,6 +258,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>

25
app/src/main/assets/app_config/configure.html

@ -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>

458
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));

128
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();
}
});
}

17
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<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter;
private GBDeviceApp selectedApp = null;
private GBDevice mGBDevice = null;
private List<GBDeviceApp> getSystemApps() {
List<GBDeviceApp> 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);
}

2
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<? extends Activity> primaryActivity = coordinator.getPrimaryActivity();
if (primaryActivity != null) {

183
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<String> 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);
}
}

3
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();
}

29
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() {

21
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;
}
}

8
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);

2
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) {

3
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";

7
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) {

8
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")) {

5
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java

@ -657,6 +657,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
// not supported
}
@Override
public void onAppConfiguration(UUID uuid, String config) {
// not supported
}
@Override
public void onScreenshotReq() {
// not supported

6
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<Pair<Integer, Object>> 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};
*/
}
}

3
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<Pair<Integer, Object>> pairs) {
return null;
/*
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeTimeStylePebbleConfig();
return new GBDeviceEvent[]{sendBytes};
*/
}
}

11
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);

24
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<Pair<Integer, Object>> pairs = new ArrayList<>();
JSONObject json = new JSONObject(config);
Iterator<String> 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() {

27
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;
}
}

5
app/src/main/res/layout/activity_external_pebble_js.xml

@ -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" />

4
app/src/main/res/menu/appmanager_context.xml

@ -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>

1
app/src/main/res/values/strings.xml

@ -224,6 +224,7 @@
<string name="appmanager_health_deactivate">Deactivate</string>