Fossil HR: added infrastructure to display on-device confirmations (#2451)

This PR aims to add the on-device connection confirmation for the Fossil HR.
This seems mandatory, since, at least on my watch, without said confirmation certain files like the configuration cannot be accesses, e.g. the time on the watch cannot be set etc.

The mystery yet to be solved is how to get the watch to not ask for a confirmation on every new connection or reconnection attempt, since that can get very annoying.

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2451
Co-authored-by: dakhnod <dakhnod@noreply.codeberg.org>
Co-committed-by: dakhnod <dakhnod@noreply.codeberg.org>
This commit is contained in:
dakhnod 2021-12-07 16:41:37 +01:00 committed by Andreas Shimokawa
parent 7d552ce41f
commit 4981aacb30
8 changed files with 153 additions and 22 deletions

View File

@ -169,6 +169,7 @@ public class FossilWatchAdapter extends WatchAdapter {
requestQueue.clear();
}
log("characteristic write failed: " + status);
GB.toast(fossilRequest.getName() + " characteristic write failed: " + status, Toast.LENGTH_SHORT, GB.ERROR);
fossilRequest = null;
queueNextRequest();
@ -583,7 +584,7 @@ public class FossilWatchAdapter extends WatchAdapter {
requestFinished = fossilRequest.isFinished();
} catch (RuntimeException e) {
if (characteristic.getUuid().toString().equals("3dda0005-957f-7d4a-34a6-74696673696d")) {
GB.log("authentication failed", GB.ERROR, null);
GB.log("authentication failed", GB.ERROR, e);
// setDeviceState(GBDevice.State.AUTHENTICATION_REQUIRED);
}else {
GB.log("error", GB.ERROR, e);

View File

@ -110,6 +110,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.application.ApplicationInformation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.application.ApplicationsListRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.async.ConfirmAppStatusRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.CheckDeviceNeedsConfirmationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.ConfirmOnDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfiguration;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest;
@ -211,9 +213,44 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
private void initializeAfterAuthentication(boolean authenticated) {
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZING));
if (!authenticated)
if (!authenticated) {
GB.toast(getContext().getString(R.string.fossil_hr_auth_failed), Toast.LENGTH_LONG, GB.ERROR);
initializeAfterWatchConfirmation(false);
return;
}
boolean shouldAuthenticateOnWatch = getDeviceSpecificPreferences().getBoolean("enable_on_device_confirmation", true);
if(!shouldAuthenticateOnWatch){
GB.toast("Skipping on-device confirmation", Toast.LENGTH_SHORT, GB.INFO);
initializeAfterWatchConfirmation(false);
return;
}
confirmOnWatch();
}
private void confirmOnWatch() {
queueWrite(new CheckDeviceNeedsConfirmationRequest() {
@Override
public void onResult(boolean needsConfirmation) {
GB.log("needs confirmation: " + needsConfirmation, GB.INFO, null);
if(needsConfirmation){
GB.toast("please confirm on device.", Toast.LENGTH_SHORT, GB.INFO);
queueWrite( new ConfirmOnDeviceRequest(){
@Override
public void onResult(boolean confirmationSuccess) {
if(!confirmationSuccess){
GB.toast("connection unconfirmed on watch, unauthenticated mode", Toast.LENGTH_LONG, GB.ERROR);
}
initializeAfterWatchConfirmation(confirmationSuccess);
}
}, true);
}else{
initializeAfterWatchConfirmation(true);
}
}
});
}
private void initializeAfterWatchConfirmation(boolean authenticated){
setNotificationConfigurations();
setQuickRepliesConfiguration();
@ -1224,22 +1261,23 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
@Override
public void onTestNewFunction() {
queueWrite(new FossilRequest() {
@Override
public boolean isFinished() {
return true;
}
try{
JSONObject data = new JSONObject()
.put("push", new JSONObject()
.put("set", new JSONObject()
.put("widgetCustom0._.config.upper_text", "0 up")
.put("widgetCustom0._.config.lower_text", "0 low")
@Override
public byte[] getStartSequence() {
return new byte[]{0x01, 0x07};
}
.put("widgetCustom1._.config.upper_text", "1 up")
.put("widgetCustom1._.config.lower_text", "1 low")
)
);
queueWrite(new JsonPutRequest(data, this));
}catch (Exception e) {
e.printStackTrace();
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d");
}
});
queueWrite(new ConfirmOnDeviceRequest());
}
public byte[] getSecretKey() throws IllegalAccessException {
@ -1483,6 +1521,14 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
}
}
@Override
public void onFindDevice(boolean start) {
super.onFindDevice(start);
if(start){
queueWrite(new ConfirmOnDeviceRequest());
}
}
private void handleDeleteNotification(byte[] value) {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);

View File

@ -0,0 +1,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public abstract class AuthenticationRequest extends FossilRequest {
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,34 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication;
import android.bluetooth.BluetoothGattCharacteristic;
public abstract class CheckDeviceNeedsConfirmationRequest extends AuthenticationRequest {
private boolean isFinished = false;
@Override
public byte[] getStartSequence() {
return new byte[]{0x01, 0x07};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
if(!characteristic.getUuid().equals(getRequestUUID())){
throw new RuntimeException("wrong characteristic responded to authentication");
}
byte[] value = characteristic.getValue();
if(value.length != 3){
throw new RuntimeException("wrong authentication response length");
}
if(value[0] != 0x03 || value[1] != 0x07){
throw new RuntimeException("wrong authentication response bytes");
}
this.onResult(value[2] == 0x00);
this.isFinished = true;
}
public abstract void onResult(boolean needsConfirmation);
@Override
public boolean isFinished() {
return isFinished;
}
}

View File

@ -0,0 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication;
import android.bluetooth.BluetoothGattCharacteristic;
public class ConfirmOnDeviceRequest extends AuthenticationRequest {
protected boolean isFinished = false;
@Override
public byte[] getStartSequence() {
return new byte[]{0x02, 0x06, 0x30, 0x75, 0x00, 0x00, 0x00};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
if(!characteristic.getUuid().equals(getRequestUUID())){
throw new RuntimeException("wrong characteristic responded to authentication");
}
byte[] value = characteristic.getValue();
if(value.length != 4){
throw new RuntimeException("wrong authentication response length");
}
if(value[0] != 0x03 || value[1] != 0x06 || value[2] != 0x00){
throw new RuntimeException("wrong authentication response bytes");
}
this.onResult(value[3] == 0x01);
this.isFinished = true;
}
public void onResult(boolean confirmationSuccess){};
@Override
public boolean isFinished() {
return isFinished;
}
}

View File

@ -37,7 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.foss
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 {
public class VerifyPrivateKeyRequest extends AuthenticationRequest {
private final FossilHRWatchAdapter adapter;
private byte[] key, randomPhoneNumber;
private boolean isFinished = false;
@ -132,9 +132,4 @@ public class VerifyPrivateKeyRequest extends FossilRequest {
return buffer.array();
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -1392,5 +1392,7 @@
<string name="watchface_dialog_widget_update_timeout">Update timeout in minutes:</string>
<string name="watchface_dialog_widget_timeout_hide_text">Hide text on timeout:</string>
<string name="watchface_dialog_widget_timeout_show_circle">Show circle on timeout:</string>
<string name="qhybrid_title_on_device_confirmation">Enable on-device pairing confirmation</string>
<string name="qhybrid_summary_on_device_confirmation">On-device pairing confirmations can get annoying. Disabling them might lose you functionality.</string>
</resources>

View File

@ -124,6 +124,12 @@
android:targetClass="nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FileManagementActivity" />
</Preference>
<SwitchPreference
android:defaultValue="true"
android:key="enable_on_device_confirmation"
android:title="@string/qhybrid_title_on_device_confirmation"
android:summary="@string/qhybrid_summary_on_device_confirmation" />
</PreferenceScreen>
</androidx.preference.PreferenceScreen>