1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 01:25:50 +01:00

Merge pull request #235 from Freeyourgadget/feature-configuration

Use external browser for configuring pebble apps
This commit is contained in:
Andreas Shimokawa 2016-03-08 10:41:46 +01:00
commit 88982a6174
25 changed files with 979 additions and 23 deletions

View File

@ -53,6 +53,7 @@
android:label="@string/preferences_miband_settings" android:label="@string/preferences_miband_settings"
android:parentActivityName=".activities.SettingsActivity" /> android:parentActivityName=".activities.SettingsActivity" />
<activity <activity
android:launchMode="singleTop"
android:name=".activities.AppManagerActivity" android:name=".activities.AppManagerActivity"
android:label="@string/title_activity_appmanager" android:label="@string/title_activity_appmanager"
android:parentActivityName=".activities.ControlCenter" /> android:parentActivityName=".activities.ControlCenter" />
@ -257,6 +258,22 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/sleep_alarm_widget_info" /> android:resource="@xml/sleep_alarm_widget_info" />
</receiver> </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> </application>
</manifest> </manifest>

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -72,6 +73,7 @@ public class AppManagerActivity extends Activity {
private final List<GBDeviceApp> appList = new ArrayList<>(); private final List<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter; private GBDeviceAppAdapter mGBDeviceAppAdapter;
private GBDeviceApp selectedApp = null; private GBDeviceApp selectedApp = null;
private GBDevice mGBDevice = null;
private List<GBDeviceApp> getSystemApps() { private List<GBDeviceApp> getSystemApps() {
List<GBDeviceApp> systemApps = new ArrayList<>(); List<GBDeviceApp> systemApps = new ArrayList<>();
@ -116,6 +118,13 @@ public class AppManagerActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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()); sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_appmanager); setContentView(R.layout.activity_appmanager);
@ -189,6 +198,14 @@ public class AppManagerActivity extends Activity {
case R.id.appmanager_health_activate: case R.id.appmanager_health_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health")); GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
return true; 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: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }

View File

@ -131,7 +131,7 @@ public class ControlCenter extends Activity {
@Override @Override
public void onItemClick(AdapterView parent, View v, int position, long id) { public void onItemClick(AdapterView parent, View v, int position, long id) {
GBDevice gbDevice = deviceList.get(position); GBDevice gbDevice = deviceList.get(position);
if (gbDevice.isConnected()) { if (gbDevice.isInitialized()) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
Class<? extends Activity> primaryActivity = coordinator.getPrimaryActivity(); Class<? extends Activity> primaryActivity = coordinator.getPrimaryActivity();
if (primaryActivity != null) { if (primaryActivity != null) {

View File

@ -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);
}
}

View File

@ -36,6 +36,8 @@ public interface EventHandler {
void onAppDelete(UUID uuid); void onAppDelete(UUID uuid);
void onAppConfiguration(UUID appUuid, String config);
void onFetchActivityData(); void onFetchActivityData();
void onReboot(); void onReboot();
@ -45,4 +47,5 @@ public interface EventHandler {
void onFindDevice(boolean start); void onFindDevice(boolean start);
void onScreenshotReq(); void onScreenshotReq();
} }

View File

@ -23,6 +23,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public class PBWInstallHandler implements InstallHandler { public class PBWInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(PBWInstallHandler.class); private static final Logger LOG = LoggerFactory.getLogger(PBWInstallHandler.class);
@ -49,15 +50,7 @@ public class PBWInstallHandler implements InstallHandler {
return; return;
} }
String hwRev = device.getHardwareVersion(); String platformName = PebbleUtils.getPlatformName(device.getHardwareVersion());
String platformName;
if (hwRev.startsWith("snowy")) {
platformName = "basalt";
} else if (hwRev.startsWith("spalding")) {
platformName = "chalk";
} else {
platformName = "aplite";
}
try { try {
mPBWReader = new PBWReader(mUri, mContext, platformName); mPBWReader = new PBWReader(mUri, mContext, platformName);
@ -173,6 +166,24 @@ public class PBWInstallHandler implements InstallHandler {
} catch (JSONException e) { } catch (JSONException e) {
LOG.error(e.getMessage(), 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() { public boolean isValid() {

View File

@ -57,6 +57,7 @@ public class PBWReader {
private short mAppVersion; private short mAppVersion;
private int mIconId; private int mIconId;
private int mFlags; private int mFlags;
private String jsConfigurationFile = null;
private JSONObject mAppKeys = null; private JSONObject mAppKeys = null;
@ -212,6 +213,20 @@ public class PBWReader {
e.printStackTrace(); e.printStackTrace();
break; 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")) { } else if (fileName.equals(platformDir + "pebble-app.bin")) {
zis.read(buffer, 0, 108); zis.read(buffer, 0, 108);
byte[] tmp_buf = new byte[32]; byte[] tmp_buf = new byte[32];
@ -327,4 +342,8 @@ public class PBWReader {
public JSONObject getAppKeysJSON() { public JSONObject getAppKeysJSON() {
return mAppKeys; return mAppKeys;
} }
public String getJsConfigurationFile() {
return jsConfigurationFile;
}
} }

View File

@ -159,6 +159,14 @@ public class GBDeviceService implements DeviceService {
invokeService(intent); 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 @Override
public void onFetchActivityData() { public void onFetchActivityData() {
Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA); Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA);

View File

@ -15,7 +15,6 @@ public interface DeviceService extends EventHandler {
String ACTION_START = PREFIX + ".action.start"; String ACTION_START = PREFIX + ".action.start";
String ACTION_CONNECT = PREFIX + ".action.connect"; String ACTION_CONNECT = PREFIX + ".action.connect";
String ACTION_NOTIFICATION = PREFIX + ".action.notification"; String ACTION_NOTIFICATION = PREFIX + ".action.notification";
String ACTION_NOTIFICATION_SMS = PREFIX + ".action.notification_sms";
String ACTION_CALLSTATE = PREFIX + ".action.callstate"; String ACTION_CALLSTATE = PREFIX + ".action.callstate";
String ACTION_SETTIME = PREFIX + ".action.settime"; String ACTION_SETTIME = PREFIX + ".action.settime";
String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo"; 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_REQUEST_SCREENSHOT = PREFIX + ".action.request_screenshot";
String ACTION_STARTAPP = PREFIX + ".action.startapp"; String ACTION_STARTAPP = PREFIX + ".action.startapp";
String ACTION_DELETEAPP = PREFIX + ".action.deleteapp"; String ACTION_DELETEAPP = PREFIX + ".action.deleteapp";
String ACTION_APP_CONFIGURE = PREFIX + ".action.app_configure";
String ACTION_INSTALL = PREFIX + ".action.install"; String ACTION_INSTALL = PREFIX + ".action.install";
String ACTION_REBOOT = PREFIX + ".action.reboot"; String ACTION_REBOOT = PREFIX + ".action.reboot";
String ACTION_HEARTRATE_TEST = PREFIX + ".action.heartrate_test"; 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_MUSIC_TRACK = "music_track";
String EXTRA_APP_UUID = "app_uuid"; String EXTRA_APP_UUID = "app_uuid";
String EXTRA_APP_START = "app_start"; String EXTRA_APP_START = "app_start";
String EXTRA_APP_CONFIG = "app_config";
String EXTRA_URI = "uri"; String EXTRA_URI = "uri";
String EXTRA_ALARMS = "alarms"; String EXTRA_ALARMS = "alarms";
String EXTRA_PERFORM_PAIR = "perform_pair"; String EXTRA_PERFORM_PAIR = "perform_pair";

View File

@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB; 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_CALLSTATE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP; 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_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; 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_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_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND;
@ -306,6 +308,11 @@ public class DeviceCommunicationService extends Service {
mDeviceSupport.onAppDelete(uuid); mDeviceSupport.onAppDelete(uuid);
break; 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: case ACTION_INSTALL:
Uri uri = intent.getParcelableExtra(EXTRA_URI); Uri uri = intent.getParcelableExtra(EXTRA_URI);
if (uri != null) { if (uri != null) {

View File

@ -178,6 +178,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onAppDelete(uuid); delegate.onAppDelete(uuid);
} }
@Override
public void onAppConfiguration(UUID uuid, String config) {
if (checkBusy("app configuration")) {
return;
}
delegate.onAppConfiguration(uuid, config);
}
@Override @Override
public void onFetchActivityData() { public void onFetchActivityData() {
if (checkBusy("fetch activity data")) { if (checkBusy("fetch activity data")) {

View File

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

View File

@ -96,18 +96,24 @@ public class AppMessageHandlerPebStyle extends AppMessageHandler {
@Override @Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) { public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
return null;
/*
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
ByteBuffer buf = ByteBuffer.allocate(encodeAck().length + encodePebStyleConfig().length); ByteBuffer buf = ByteBuffer.allocate(encodeAck().length + encodePebStyleConfig().length);
buf.put(encodeAck()); buf.put(encodeAck());
buf.put(encodePebStyleConfig()); buf.put(encodePebStyleConfig());
sendBytes.encodedBytes = buf.array(); sendBytes.encodedBytes = buf.array();
return new GBDeviceEvent[]{sendBytes}; return new GBDeviceEvent[]{sendBytes};
*/
} }
@Override @Override
public GBDeviceEvent[] pushMessage() { public GBDeviceEvent[] pushMessage() {
return null;
/*
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodePebStyleConfig(); sendBytes.encodedBytes = encodePebStyleConfig();
return new GBDeviceEvent[]{sendBytes}; return new GBDeviceEvent[]{sendBytes};
*/
} }
} }

View File

@ -100,8 +100,11 @@ public class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
@Override @Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) { public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
return null;
/*
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeTimeStylePebbleConfig(); sendBytes.encodedBytes = encodeTimeStylePebbleConfig();
return new GBDeviceEvent[]{sendBytes}; return new GBDeviceEvent[]{sendBytes};
*/
} }
} }

View File

@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public class PebbleIoThread extends GBDeviceIoThread { public class PebbleIoThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class); private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class);
@ -577,15 +578,7 @@ public class PebbleIoThread extends GBDeviceIoThread {
return; return;
} }
String hwRev = gbDevice.getHardwareVersion(); String platformName = PebbleUtils.getPlatformName(gbDevice.getHardwareVersion());
String platformName;
if (hwRev.startsWith("snowy")) {
platformName = "basalt";
} else if (hwRev.startsWith("spalding")) {
platformName = "chalk";
} else {
platformName = "aplite";
}
try { try {
mPBWReader = new PBWReader(uri, getContext(), platformName); mPBWReader = new PBWReader(uri, getContext(), platformName);

View File

@ -1,8 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.net.Uri; import android.net.Uri;
import android.util.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
@ -37,6 +43,24 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
getDeviceIOThread().installApp(uri, 0); 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 @Override
public void onHeartRateTest() { public void onHeartRateTest() {

View File

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

View File

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

View File

@ -12,5 +12,7 @@
<item <item
android:id="@+id/appmanager_health_deactivate" android:id="@+id/appmanager_health_deactivate"
android:title="@string/appmanager_health_deactivate"/> android:title="@string/appmanager_health_deactivate"/>
<item
android:id="@+id/appmanager_app_configure"
android:title="@string/app_configure"/>
</menu> </menu>

View File

@ -224,6 +224,7 @@
<string name="appmanager_health_deactivate">Deactivate</string> <string name="appmanager_health_deactivate">Deactivate</string>
<string name="authenticating">authenticating</string> <string name="authenticating">authenticating</string>
<string name="authentication_required">authentication required</string> <string name="authentication_required">authentication required</string>
<string name="app_configure">Configure</string>
<string name="appwidget_text">Zzz</string> <string name="appwidget_text">Zzz</string>
<string name="add_widget">Add widget</string> <string name="add_widget">Add widget</string>

View File

@ -90,6 +90,11 @@ public class TestDeviceSupport extends AbstractDeviceSupport {
} }
@Override
public void onAppConfiguration(UUID appUuid, String config) {
}
@Override @Override
public void onFetchActivityData() { public void onFetchActivityData() {