1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 12:56:49 +01:00

[Huawei] Implement remote camera shutter

This commit is contained in:
Martin.JM 2024-05-05 21:16:01 +02:00 committed by José Rebelo
parent 4c4ba623c4
commit 9ba96231c3
9 changed files with 267 additions and 1 deletions

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> createRequest() throws RequestCreationException {
try {
return new CameraRemote.CameraRemoteSetup.Request(
supportProvider.getParamsProvider(),
this.event
).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -2842,6 +2842,7 @@
<string name="pref_title_cycling_persistence_interval">Persistence interval</string>
<string name="chart_cycling_point_label_distance">Today: %.1f km\nTotal: %.1f km</string>
<string name="chart_cycling_point_label_speed">%.1f km/h</string>
<string name="toast_setting_requires_reconnect">This setting will take effect after a reconnect</string>
<!-- Camera strings -->
<string name="open_camera">Open Camera</string>