diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java index 92a6fe7db..19d123ea9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity; import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; @@ -220,6 +221,10 @@ public class HuaweiCoordinator { deviceSpecificSettings.addRootScreen(R.xml.devicesettings_disable_find_phone_with_dnd); deviceSpecificSettings.addRootScreen(R.xml.devicesettings_allow_accept_reject_calls); + // Camera control + if (supportsCameraRemote()) + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_camera_remote); + // Time if (supportsDateFormat()) { final List dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME); @@ -281,6 +286,10 @@ public class HuaweiCoordinator { return supportsCommandForService(0x01, 0x1d); } + public boolean supportsCameraRemote() { + return supportsCommandForService(0x01, 0x29) && CameraActivity.supportsCamera(); + } + public boolean supportsAcceptAgreement() { return supportsCommandForService(0x01, 0x30); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java index 595871814..289745423 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java @@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; @@ -430,6 +431,11 @@ public class HuaweiPacket { return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this); case DeviceConfig.WearStatus.id: return new DeviceConfig.WearStatus.Response(paramsProvider).fromPacket(this); + + // Camera remote has same ID as DeviceConfig + case CameraRemote.CameraRemoteStatus.id: + return new CameraRemote.CameraRemoteStatus.Response(paramsProvider).fromPacket(this); + default: this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/CameraRemote.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/CameraRemote.java new file mode 100644 index 000000000..232d939ca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/CameraRemote.java @@ -0,0 +1,110 @@ +/* Copyright (C) 2024 Martin.JM + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class CameraRemote { + public static final byte id = 0x01; + + public static class CameraRemoteSetup { + public static final byte id = 0x2a; + + public static class Request extends HuaweiPacket { + public enum Event { + ENABLE_CAMERA, + CAMERA_STARTED, + CAMERA_STOPPED + } + + public Request(ParamsProvider paramsProvider, Event event) { + super(paramsProvider); + + this.serviceId = CameraRemote.id; + this.commandId = id; + + this.tlv = new HuaweiTLV(); + switch (event) { + case ENABLE_CAMERA: + this.tlv.put(0x01, (byte) 0x00); + break; + case CAMERA_STARTED: + this.tlv.put(0x01, (byte) 0x01); + break; + case CAMERA_STOPPED: + this.tlv.put(0x01, (byte) 0x02); + break; + } + + this.complete = true; + this.isEncrypted = true; + } + } + } + + public static class CameraRemoteStatus { + public static final byte id = 0x29; + + public static class Request extends HuaweiPacket { + // All responses are async, and must be ACK-ed + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = CameraRemote.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x7f, 0x186A0); + + this.complete = true; + this.isEncrypted = true; + } + } + + public static class Response extends HuaweiPacket { + public enum Event { + OPEN_CAMERA, + TAKE_PICTURE, + CLOSE_CAMERA + } + + public Event event; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = CameraRemote.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + switch (this.tlv.getByte(0x01)) { + case 1: + this.event = Event.OPEN_CAMERA; + break; + case 2: + this.event = Event.TAKE_PICTURE; + break; + case 3: + this.event = Event.CLOSE_CAMERA; + break; + } + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java index 8a71f6f99..77b51bea8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java @@ -38,12 +38,15 @@ import java.util.HashMap; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; @@ -111,6 +114,7 @@ public class AsynchronousResponse { handleGpsRequest(response); handleFileUpload(response); handleWatchface(response); + handleCameraRemote(response); } catch (Request.ResponseParseException e) { LOG.error("Response parse exception", e); } @@ -500,4 +504,37 @@ public class AsynchronousResponse { support.setGps(((GpsAndTime.GpsStatus.Response) response).enableGps); } } + + private void handleCameraRemote(HuaweiPacket response) { + if (response.serviceId == CameraRemote.id && response.commandId == CameraRemote.CameraRemoteStatus.id) { + if (!(response instanceof CameraRemote.CameraRemoteStatus.Response)) { + // TODO: exception? + return; + } + + if (!CameraActivity.supportsCamera()) { + LOG.error("No camera present"); + // TODO: Toast? + return; + } + + switch (((CameraRemote.CameraRemoteStatus.Response) response).event) { + case OPEN_CAMERA: + GBDeviceEventCameraRemote openCameraEvent = new GBDeviceEventCameraRemote(); + openCameraEvent.event = GBDeviceEventCameraRemote.Event.OPEN_CAMERA; + support.evaluateGBDeviceEvent(openCameraEvent); + break; + case TAKE_PICTURE: + GBDeviceEventCameraRemote takePictureEvent = new GBDeviceEventCameraRemote(); + takePictureEvent.event = GBDeviceEventCameraRemote.Event.TAKE_PICTURE; + support.evaluateGBDeviceEvent(takePictureEvent); + break; + case CLOSE_CAMERA: + GBDeviceEventCameraRemote closeCameraEvent = new GBDeviceEventCameraRemote(); + closeCameraEvent.event = GBDeviceEventCameraRemote.Event.CLOSE_CAMERA; + support.evaluateGBDeviceEvent(closeCameraEvent); + break; + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java index 78bc704e7..2b1f2139c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -164,4 +165,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport { supportProvider.onAppDelete(uuid); } + @Override + public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) { + supportProvider.onCameraStatusChange(event, filename); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java index 44a8689a5..6af5fa401 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -172,5 +173,8 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport { supportProvider.onAppDelete(uuid); } - + @Override + public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) { + supportProvider.onCameraStatusChange(event, filename); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index f272e97ff..6c7349c27 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -21,6 +21,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; import android.widget.Toast; import androidx.annotation.NonNull; @@ -44,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator; @@ -52,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupp import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; @@ -87,6 +91,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetG import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest; @@ -744,6 +749,11 @@ public class HuaweiSupportProvider { GetWatchfaceParams getWatchfaceParams = new GetWatchfaceParams(this); getWatchfaceParams.doPerform(); } + + if (getHuaweiCoordinator().supportsCameraRemote() && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false)) { + SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.ENABLE_CAMERA); + sendCameraRemoteSetupEvent.doPerform(); + } } catch (IOException e) { GB.toast(getContext(), "Initialize dynamic services of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); @@ -967,6 +977,15 @@ public class HuaweiSupportProvider { case ActivityUser.PREF_USER_STEPS_GOAL: new SendFitnessGoalRequest(this).doPerform(); break; + case DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE: + if (GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false)) { + SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.ENABLE_CAMERA); + sendCameraRemoteSetupEvent.doPerform(); + } else { + // Somehow it is impossible to disable the camera remote + // But it will disappear after reconnection - until it is enabled again + GB.toast(context, context.getString(R.string.toast_setting_requires_reconnect), Toast.LENGTH_SHORT, GB.INFO); + } } } catch (IOException e) { // TODO: Use translatable string @@ -1912,4 +1931,32 @@ public class HuaweiSupportProvider { huaweiWatchfaceManager.deleteWatchface(uuid); } + public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) { + if (event == GBDeviceEventCameraRemote.Event.OPEN_CAMERA) { + // Somehow a delay is necessary for the watch + new Handler(GBApplication.getContext().getMainLooper()).postDelayed( + new Runnable() { + @Override + public void run() { + SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(HuaweiSupportProvider.this, CameraRemote.CameraRemoteSetup.Request.Event.CAMERA_STARTED); + try { + sendCameraRemoteSetupEvent.doPerform(); + } catch (IOException e) { + GB.toast("Failed to send open camera request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to send open camera request", e); + } + } + }, + 3000 + ); + } else if (event == GBDeviceEventCameraRemote.Event.CLOSE_CAMERA) { + SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent2 = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.CAMERA_STOPPED); + try { + sendCameraRemoteSetupEvent2.doPerform(); + } catch (IOException e) { + GB.toast("Failed to send open camera request", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Failed to send open camera request", e); + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendCameraRemoteSetupEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendCameraRemoteSetupEvent.java new file mode 100644 index 000000000..4e524c598 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendCameraRemoteSetupEvent.java @@ -0,0 +1,47 @@ +/* Copyright (C) 2024 Martin.JM + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SendCameraRemoteSetupEvent extends Request { + + CameraRemote.CameraRemoteSetup.Request.Event event; + + public SendCameraRemoteSetupEvent(HuaweiSupportProvider support, CameraRemote.CameraRemoteSetup.Request.Event event) { + super(support); + this.serviceId = CameraRemote.id; + this.commandId = CameraRemote.CameraRemoteSetup.id; + this.event = event; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new CameraRemote.CameraRemoteSetup.Request( + supportProvider.getParamsProvider(), + this.event + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3945d0e45..f763b0fca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2842,6 +2842,7 @@ Persistence interval Today: %.1f km\nTotal: %.1f km %.1f km/h + This setting will take effect after a reconnect Open Camera