1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-25 11:26:47 +01:00

added menu handling and error status

This commit is contained in:
dakhnod 2019-12-31 03:14:20 +01:00
parent 5ca4816b01
commit c4d63a80e1
23 changed files with 756 additions and 56 deletions

View File

@ -505,6 +505,9 @@
<activity
android:name=".devices.qhybrid.QHybridAppChoserActivity"
android:exported="true" />
<activity
android:name=".devices.qhybrid.HRConfigActivity"
android:exported="true" />
</application>
</manifest>

View File

@ -312,7 +312,7 @@ public class ConfigActivity extends AbstractGBActivity {
});
device = GBApplication.app().getDeviceManager().getSelectedDevice();
if (device == null || device.getType() != DeviceType.FOSSILQHYBRID) {
if (device == null || device.getType() != DeviceType.FOSSILQHYBRID || device.getFirmwareVersion().charAt(2) != '0') {
setSettingsError(getString(R.string.watch_not_connected));
} else {
updateSettings();

View File

@ -0,0 +1,191 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONArray;
import org.json.JSONException;
import org.w3c.dom.Text;
import java.sql.Array;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
public class HRConfigActivity extends AbstractGBActivity implements View.OnClickListener, DialogInterface.OnClickListener, AdapterView.OnItemClickListener {
private SharedPreferences sharedPreferences;
private ActionListAdapter actionListAdapter;
private ArrayList<MenuAction> menuActions = new ArrayList<>();
static public final String CONFIG_KEY_Q_ACTIONS = "Q_ACTIONS";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_hr_settings);
findViewById(R.id.qhybrid_action_add).setOnClickListener(this);
sharedPreferences = GBApplication.getPrefs().getPreferences();
ListView actionListView = findViewById(R.id.qhybrid_action_list);
actionListAdapter = new ActionListAdapter(menuActions);
actionListView.setAdapter(actionListAdapter);
actionListView.setOnItemClickListener(this);
updateSettings();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.qhybrid_action_add) {
final EditText input = new EditText(this);
input.setId(0);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
input.setLayoutParams(lp);
new AlertDialog.Builder(this)
.setView(input)
.setNegativeButton("cancel", null)
.setPositiveButton("ok", this)
.setTitle("create action")
.show();
}
}
private void updateSettings() {
JSONArray actionArray = null;
try {
actionArray = new JSONArray(sharedPreferences.getString(CONFIG_KEY_Q_ACTIONS, "[]"));
menuActions.clear();
for (int i = 0; i < actionArray.length(); i++)
menuActions.add(new MenuAction(actionArray.getString(i)));
actionListAdapter.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onClick(DialogInterface dialog, int which) {
EditText actionEditText = ((AlertDialog) dialog).findViewById(0);
String action = actionEditText.getText().toString();
try {
JSONArray actionArray = new JSONArray(sharedPreferences.getString(CONFIG_KEY_Q_ACTIONS, "[]"));
actionArray.put(action);
sharedPreferences.edit().putString(CONFIG_KEY_Q_ACTIONS, actionArray.toString()).apply();
updateSettings();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
final EditText input = new EditText(this);
input.setId(0);
input.setText(((TextView) view).getText());
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
input.setLayoutParams(lp);
new AlertDialog.Builder(this)
.setView(input)
.setNegativeButton("delete", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
menuActions.remove(position);
putActionItems(menuActions);
updateSettings();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
}
})
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
menuActions.get(position).setAction(input.getText().toString());
putActionItems(menuActions);
updateSettings();
LocalBroadcastManager.getInstance(HRConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
}
})
.setTitle("edit action")
.show();
}
private void putActionItems(List<MenuAction> actions){
JSONArray array = new JSONArray();
for (MenuAction action : actions) array.put(action.getAction());
sharedPreferences.edit().putString(CONFIG_KEY_Q_ACTIONS, array.toString()).apply();
}
class MenuAction {
private String action;
public MenuAction(String action) {
this.action = action;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}
class ActionListAdapter extends ArrayAdapter<MenuAction> {
public ActionListAdapter(@NonNull ArrayList<MenuAction> objects) {
super(HRConfigActivity.this, 0, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
if (convertView == null) convertView = new TextView(getContext());
TextView view = (TextView) convertView;
view.setText(getItem(position).getAction());
// view.setTextColor(Color.WHITE);
view.setTextSize(30);
return view;
}
}
}

View File

@ -145,7 +145,9 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return ConfigActivity.class;
GBDevice connectedDevice = GBApplication.app().getDeviceManager().getSelectedDevice();
boolean isHR = connectedDevice.getFirmwareVersion().charAt(2) == '1';
return isHR ? HRConfigActivity.class : ConfigActivity.class;
}
@Override

View File

@ -94,4 +94,7 @@ public abstract class WatchAdapter {
}
return s.substring(0, s.length() - 1) + "\n";
}
public void setCommuteMenuMessage(String message, boolean finished) {
}
}

View File

@ -86,7 +86,7 @@ public class FossilWatchAdapter extends WatchAdapter {
private int lastButtonIndex = -1;
Logger logger = LoggerFactory.getLogger(getClass());
Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
public FossilWatchAdapter(QHybridSupport deviceSupport) {
super(deviceSupport);

View File

@ -1,16 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HRConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@ -18,13 +26,17 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.CurrentStepCountConfigItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.Image;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImagesPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.menu.SetCommuteMenuMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FossilHRWatchAdapter extends FossilWatchAdapter {
private byte[] secretKey = new byte[]{(byte) 0x60, (byte) 0x26, (byte) 0xB7, (byte) 0xFD, (byte) 0xB2, (byte) 0x6D, (byte) 0x05, (byte) 0x5E, (byte) 0xDA, (byte) 0xF7, (byte) 0x4B, (byte) 0x49, (byte) 0x98, (byte) 0x78, (byte) 0x02, (byte) 0x38};
@ -68,18 +80,18 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
e.printStackTrace();
} // icons
queueWrite(new NotificationFilterPutHRRequest(new NotificationHRConfiguration[]{
new NotificationHRConfiguration("com.whatsapp", -1),
new NotificationHRConfiguration("asdasdasdasdasd", -1),
// new NotificationHRConfiguration("twitter", -1),
}, this));
// queueWrite(new NotificationFilterPutHRRequest(new NotificationHRConfiguration[]{
// new NotificationHRConfiguration("com.whatsapp", -1),
// new NotificationHRConfiguration("asdasdasdasdasd", -1),
// // new NotificationHRConfiguration("twitter", -1),
// }, this));
queueWrite(new PlayNotificationRequest("com.whatsapp", "WhatsAp", "wHATSaPP", this));
queueWrite(new PlayNotificationRequest("twitterrrr", "Twitterr", "tWITTER", this));
// queueWrite(new PlayNotificationRequest("com.whatsapp", "WhatsAp", "wHATSaPP", this));
// queueWrite(new PlayNotificationRequest("twitterrrr", "Twitterr", "tWITTER", this));
syncSettings();
queueWrite(new ButtonConfigurationPutRequest(this));
overwriteButtons(null);
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED));
}
@ -91,6 +103,30 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
));
}
@Override
public void setTime() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
queueWrite(
new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest(
new ConfigurationPutRequest.TimeConfigItem(
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
(short) (millis % 1000),
(short) ((zone.getRawOffset() + (zone.inDaylightTime(new Date()) ? 1 : 0)) / 60000)
),
this), false
);
}
private void setBackgroundImages(Image background, Image[] complications){
background.setAngle(0);
background.setDistance(0);
background.setIndexZ(0);
queueWrite(new ImagesPutRequest(new Image[]{background}, this));
}
@Override
public void onFetchActivityData() {
syncSettings();
@ -100,6 +136,8 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
negotiateSymmetricKey();
queueWrite(new ConfigurationGetRequest(this));
setTime();
}
@Override
@ -138,6 +176,24 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
return watchRandomNumber;
}
@Override
public void overwriteButtons(String jsonConfigString) {
try {
JSONArray jsonArray = new JSONArray(
GBApplication.getPrefs().getString(HRConfigActivity.CONFIG_KEY_Q_ACTIONS, "[]")
);
String[] menuItems = new String[jsonArray.length()];
for(int i = 0; i < jsonArray.length(); i++) menuItems[i] = jsonArray.getString(i);
queueWrite(new ButtonConfigurationPutRequest(
menuItems,
this
));
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
protected void handleBackgroundCharacteristic(BluetoothGattCharacteristic characteristic) {
super.handleBackgroundCharacteristic(characteristic);
@ -149,12 +205,29 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
try {
JSONObject requestJson = new JSONObject(new String(value, 3, value.length - 3));
String action = requestJson.getJSONObject("commuteApp._.config.commute_info")
String action = requestJson.getJSONObject("req").getJSONObject("commuteApp._.config.commute_info")
.getString("dest");
String startStop = requestJson.getJSONObject("req").getJSONObject("commuteApp._.config.commute_info")
.getString("action");
if(startStop.equals("stop")){
// overwriteButtons(null);
return;
}
queueWrite(new SetCommuteMenuMessage("Anfrage wird weitergeleitet...", false, this));
Intent menuIntent = new Intent(QHybridSupport.QHYBRID_EVENT_COMMUTE_MENU);
menuIntent.putExtra("EXTRA_ACTION", action);
getContext().sendBroadcast(menuIntent);
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void setCommuteMenuMessage(String message, boolean finished) {
queueWrite(new SetCommuteMenuMessage(message, finished, this));
}
}

View File

@ -24,6 +24,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public class FileCloseRequest extends FossilRequest {
private boolean isFinished = false;
@ -64,7 +65,7 @@ public class FileCloseRequest extends FossilRequest {
byte status = buffer.get(3);
if(status != 0) throw new RuntimeException("wrong response status");
if(status != 0) throw new RuntimeException("wrong response status: " + ResultCode.fromCode(status) + " (" + status + ")");
this.isFinished = true;

View File

@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public abstract class FileGetRequest extends FossilRequest {
private short handle;
@ -75,7 +76,7 @@ public abstract class FileGetRequest extends FossilRequest {
byte status = buffer.get(3);
if(status != 0){
throw new RuntimeException("FileGet error: " + status);
throw new RuntimeException("FileGet error: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if(this.handle != handle){

View File

@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public class FileLookupRequest extends FossilRequest {
private short handle = -1;
@ -82,7 +83,7 @@ public class FileLookupRequest extends FossilRequest {
byte status = buffer.get(3);
if(status != 0){
throw new RuntimeException("file lookup error: " + status);
throw new RuntimeException("file lookup error: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if(this.handle != handle){

View File

@ -27,6 +27,7 @@ import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
public class FilePutRequest extends FossilRequest {
@ -100,7 +101,7 @@ public class FilePutRequest extends FossilRequest {
byte status = value[3];
if (status != 0) {
throw new RuntimeException("upload status: " + status);
throw new RuntimeException("upload status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if (handle != this.handle) {
@ -146,7 +147,7 @@ public class FilePutRequest extends FossilRequest {
if (status != 0) {
onFilePut(false);
throw new RuntimeException("wrong closing status: " + status);
throw new RuntimeException("wrong closing status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
this.state = UploadState.UPLOADED;

View File

@ -22,6 +22,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public class FileVerifyRequest extends FossilRequest {
private boolean isFinished = false;
@ -64,7 +65,7 @@ public class FileVerifyRequest extends FossilRequest {
byte status = buffer.get(3);
if(status != 0) throw new RuntimeException("wrong response status");
if(status != 0) throw new RuntimeException("wrong response status: " + ResultCode.fromCode(status) + " (" + status + ")");
this.isFinished = true;

View File

@ -19,6 +19,7 @@ import javax.crypto.spec.SecretKeySpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public class VerifyPrivateKeyRequest extends FossilRequest {
private final FossilHRWatchAdapter adapter;
@ -29,7 +30,6 @@ public class VerifyPrivateKeyRequest extends FossilRequest {
this.adapter = adapter;
this.key = key;
adapter.setPhoneRandomNumber(randomPhoneNumber);
}
@Override
@ -62,6 +62,7 @@ public class VerifyPrivateKeyRequest extends FossilRequest {
System.arraycopy(result, 0, watchRandomNumber, 0, 8);
adapter.setWatchRandomNumber(watchRandomNumber);
adapter.setPhoneRandomNumber(randomPhoneNumber);
cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
@ -81,7 +82,8 @@ public class VerifyPrivateKeyRequest extends FossilRequest {
throw new RuntimeException(e);
}
} else if (value[1] == 2) {
if (value[2] != 0) throw new RuntimeException("Authentication error: " + value[2]);
if (value[2] != 0) throw new RuntimeException("Authentication error: " + ResultCode.fromCode(value[2]) + " (" + value[2] + ")");
this.isFinished = true;
}

View File

@ -11,37 +11,16 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ButtonConfigurationPutRequest extends JsonPutRequest {
public ButtonConfigurationPutRequest(FossilWatchAdapter adapter) {
super((short) 0x0500, createObject(), adapter);
public ButtonConfigurationPutRequest(String[] menuItems, FossilWatchAdapter adapter) {
super((short) 0x0500, createObject(menuItems), adapter);
}
private static JSONObject createObject() {
private static JSONObject createObject(String[] menuItems) {
try {
return new JSONObject()
.put("push", new JSONObject()
.put("set", new JSONObject()
.put("commuteApp._.config.destinations", new JSONArray()
.put("LAMP 1")
.put("LAMP 3")
.put("LAMP 4")
.put("LAMP 5")
.put("LAMP 6")
.put("LAMP 7")
.put("LAMP 8")
.put("LAMP 9")
.put("LAMP 10")
.put("LAMP 11")
.put("LAMP 12")
.put("LAMP 13")
.put("LAMP 14")
.put("LAMP 8")
.put("LAMP 9")
.put("LAMP 10")
.put("LAMP 11")
.put("LAMP 12")
.put("LAMP 13")
.put("LAMP 14")
)
.put("commuteApp._.config.destinations", new JSONArray(menuItems))
.put("master._.config.buttons", new JSONArray()
.put(new JSONObject()
.put("name", "commuteApp")

View File

@ -0,0 +1,58 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedPutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.ConfigItem;
public class ConfigurationPutRequest extends FileEncryptedPutRequest {
private static HashMap<Short, Class<? extends ConfigItem>> itemsById = new HashMap<>();
public ConfigurationPutRequest(ConfigItem item, FossilHRWatchAdapter adapter) {
super((short) 0x0800, createFileContent(new ConfigItem[]{item}), adapter);
}
public ConfigurationPutRequest(ConfigItem[] items, FossilHRWatchAdapter adapter) {
super((short) 0x0800, createFileContent(items), adapter);
}
private static byte[] createFileContent(ConfigItem[] items) {
int overallSize = 0;
for(ConfigItem item : items){
overallSize += item.getItemSize() + 3;
}
ByteBuffer buffer = ByteBuffer.allocate(overallSize);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for(ConfigItem item : items){
buffer.putShort(item.getId());
buffer.put((byte) item.getItemSize());
buffer.put(item.getContent());
}
return buffer.array();
}
}

View File

@ -86,7 +86,7 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
byte status = buffer.get(3);
if(status != 0){
throw new RuntimeException("FileGet error: " + status);
throw new RuntimeException("FileGet error: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if(this.handle != handle){
@ -111,7 +111,7 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
int crcExpected = buffer.getInt(8);
if((int) crc.getValue() != crcExpected){
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
throw new RuntimeException("crc: " + crc.getValue() + " expected: " + crcExpected);
}
this.handleFileData(this.fileData);

View File

@ -0,0 +1,271 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FileEncryptedPutRequest extends FossilRequest {
public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED}
public UploadState state;
private ArrayList<byte[]> packets = new ArrayList<>();
private short handle;
private FossilHRWatchAdapter adapter;
private byte[] file;
private int fullCRC;
public FileEncryptedPutRequest(short handle, byte[] file, FossilHRWatchAdapter adapter) {
this.handle = handle;
this.adapter = adapter;
int fileLength = file.length + 16;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(1, handle);
buffer.putInt(3, 0);
buffer.putInt(7, fileLength);
buffer.putInt(11, fileLength);
this.data = buffer.array();
this.file = file;
state = UploadState.INITIALIZED;
}
public short getHandle() {
return handle;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
int responseType = value[0] & 0x0F;
log("response: " + responseType);
switch (responseType) {
case 3: {
if (value.length != 5 || (value[0] & 0x0F) != 3) {
throw new RuntimeException("wrong answer header");
}
state = UploadState.UPLOADING;
TransactionBuilder transactionBuilder = new TransactionBuilder("file upload");
BluetoothGattCharacteristic uploadCharacteristic = adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0004-957f-7d4a-34a6-74696673696d"));
this.prepareFilePackets(this.file);
SecretKeySpec keySpec = new SecretKeySpec(this.adapter.getSecretKey(), "AES");
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] fileIV = new byte[16];
byte[] phoneRandomNumber = adapter.getPhoneRandomNumber();
byte[] watchRandomNumber = adapter.getWatchRandomNumber();
System.arraycopy(phoneRandomNumber, 0, fileIV, 2, 6);
System.arraycopy(watchRandomNumber, 0, fileIV, 9, 7);
fileIV[7]++;
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(fileIV));
for (byte[] packet : packets) {
byte[] result = cipher.doFinal(packet);
transactionBuilder.write(uploadCharacteristic, result);
}
}catch (Exception e){
GB.toast("error encrypting file", Toast.LENGTH_LONG, GB.ERROR, e);
}
transactionBuilder.queue(adapter.getDeviceSupport().getQueue());
break;
}
case 8: {
if (value.length == 4) return;
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
int crc = buffer.getInt(8);
byte status = value[3];
if (status != 0) {
throw new RuntimeException("upload status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if (handle != this.handle) {
throw new RuntimeException("wrong response handle");
}
if (crc != this.fullCRC) {
throw new RuntimeException("file upload exception: wrong crc");
}
ByteBuffer buffer2 = ByteBuffer.allocate(3);
buffer2.order(ByteOrder.LITTLE_ENDIAN);
buffer2.put((byte) 4);
buffer2.putShort(this.handle);
new TransactionBuilder("file close")
.write(
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
buffer2.array()
)
.queue(adapter.getDeviceSupport().getQueue());
this.state = UploadState.CLOSING;
break;
}
case 4: {
if (value.length == 9) return;
if (value.length != 4 || (value[0] & 0x0F) != 4) {
throw new RuntimeException("wrong file closing header");
}
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
if (handle != this.handle) {
onFilePut(false);
throw new RuntimeException("wrong file closing handle");
}
byte status = buffer.get(3);
if (status != 0) {
onFilePut(false);
throw new RuntimeException("wrong closing status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
this.state = UploadState.UPLOADED;
onFilePut(true);
log("uploaded file");
break;
}
case 9: {
this.onFilePut(false);
throw new RuntimeException("file put timeout");
/*timeout = true;
ByteBuffer buffer2 = ByteBuffer.allocate(3);
buffer2.order(ByteOrder.LITTLE_ENDIAN);
buffer2.put((byte) 4);
buffer2.putShort(this.handle);
new TransactionBuilder("file close")
.write(
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
buffer2.array()
)
.queue(adapter.getDeviceSupport().getQueue());
this.state = UploadState.CLOSING;
break;*/
}
}
}
}
@Override
public boolean isFinished() {
return this.state == UploadState.UPLOADED;
}
private void prepareFilePackets(byte[] file) {
int maxPacketSize = adapter.getMTU() - 4;
ByteBuffer buffer = ByteBuffer.allocate(file.length + 12 + 4);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort(handle);
buffer.put((byte) 2);
buffer.put((byte) 0);
buffer.putInt(0);
buffer.putInt(file.length);
buffer.put(file);
CRC32C crc = new CRC32C();
crc.update(file,0,file.length);
buffer.putInt((int) crc.getValue());
byte[] data = buffer.array();
CRC32 fullCRC = new CRC32();
fullCRC.update(data);
this.fullCRC = (int) fullCRC.getValue();
int packetCount = (int) Math.ceil(data.length / (float) maxPacketSize);
for (int i = 0; i < packetCount; i++) {
int currentPacketLength = Math.min(maxPacketSize, data.length - i * maxPacketSize);
byte[] packet = new byte[currentPacketLength + 1];
packet[0] = (byte) i;
System.arraycopy(data, i * maxPacketSize, packet, 1, currentPacketLength);
packets.add(packet);
}
}
public void onFilePut(boolean success) {
}
@Override
public byte[] getStartSequence() {
return new byte[]{0x03};
}
@Override
public int getPayloadLength() {
return 15;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -87,7 +87,7 @@ public class FilePutRawRequest extends FossilRequest {
byte status = value[3];
if (status != 0) {
throw new RuntimeException("upload status: " + status);
throw new RuntimeException("upload status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
if (handle != this.handle) {
@ -133,7 +133,7 @@ public class FilePutRawRequest extends FossilRequest {
if (status != 0) {
onFilePut(false);
throw new RuntimeException("wrong closing status: " + status);
throw new RuntimeException("wrong closing status: " + ResultCode.fromCode(status) + " (" + status + ")");
}
this.state = UploadState.UPLOADED;

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file;
public enum ResultCode {
SUCCESS(0),
INVALID_OPERATION_DATA(1),
OPERATION_IN_PROGRESS(2),
MISS_PACKET(3),
SOCKET_BUSY(4),
VERIFICATION_FAIL(5),
OVERFLOW(6),
SIZE_OVER_LIMIT(7),
FIRMWARE_INTERNAL_ERROR(128),
FIRMWARE_INTERNAL_ERROR_NOT_OPEN(129),
FIRMWARE_INTERNAL_ERROR_ACCESS_ERROR(130),
FIRMWARE_INTERNAL_ERROR_NOT_FOUND(131),
FIRMWARE_INTERNAL_ERROR_NOT_VALID(132),
FIRMWARE_INTERNAL_ERROR_ALREADY_CREATE(133),
FIRMWARE_INTERNAL_ERROR_NOT_ENOUGH_MEMORY(134),
FIRMWARE_INTERNAL_ERROR_NOT_IMPLEMENTED(135),
FIRMWARE_INTERNAL_ERROR_NOT_SUPPORT(136),
FIRMWARE_INTERNAL_ERROR_SOCKET_BUSY(137),
FIRMWARE_INTERNAL_ERROR_SOCKET_ALREADY_OPEN(138),
FIRMWARE_INTERNAL_ERROR_INPUT_DATA_INVALID(139),
FIRMWARE_INTERNAL_NOT_AUTHENTICATE(140),
FIRMWARE_INTERNAL_SIZE_OVER_LIMIT(141),
UNKNOWN(-1);
int code;
ResultCode(int code) {
this.code = code;
}
public static ResultCode fromCode(int code){
for (ResultCode resultCode : ResultCode.values()){
if(resultCode.code == code) return resultCode;
}
return UNKNOWN;
}
}

View File

@ -37,4 +37,36 @@ public class Image {
}
return null;
}
public int getAngle() {
return angle;
}
public void setAngle(int angle) {
this.angle = angle;
}
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
public int getIndexZ() {
return indexZ;
}
public void setIndexZ(int indexZ) {
this.indexZ = indexZ;
}
public String getImageFile() {
return imageFile;
}
public void setImageFile(String imageFile) {
this.imageFile = imageFile;
}
}

View File

@ -9,7 +9,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
public class ImagesPutRequest extends JsonPutRequest {
public ImagesPutRequest(Image[] images, FossilWatchAdapter adapter) {
super((short) 0x0501, prepareObject(images), adapter);
super((short) 0x0500, prepareObject(images), adapter);
}
private static JSONObject prepareObject(Image[] images){

View File

@ -22,6 +22,8 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
public class DownloadFileRequest extends FileRequest {
ByteBuffer buffer = null;
public byte[] file = null;
@ -69,7 +71,7 @@ public class DownloadFileRequest extends FileRequest {
this.status = buffer1.get(3);
short realHandle = buffer1.getShort(1);
if(status != 0){
log("wrong status: " + status);
log("wrong status: " + ResultCode.fromCode(status) + " (" + status + ")");
}else if(realHandle != fileHandle){
log("wrong handle: " + realHandle);
completed = true;

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="1">
<ListView
android:layout_width="match_parent"
android:layout_weight="0.4"
android:layout_height="0dp"
android:id="@+id/qhybrid_action_list"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="add action"
android:id="@+id/qhybrid_action_add"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="top button single press"
android:id="@+id/qhybrid_button_top_single_press"
android:textSize="20dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="middle button single press"
android:id="@+id/qhybrid_button_middle_single_press"
android:textSize="20dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="bottom button single press"
android:id="@+id/qhybrid_button_bottom_single_press"
android:textSize="20dp"/>
</LinearLayout>