/* Copyright (C) 2018-2024 Vadim Kaushan 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.id115; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Constants; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class SendNotificationOperation extends AbstractID115Operation { private static final Logger LOG = LoggerFactory.getLogger(SendNotificationOperation.class); byte[] currentNotificationBuffer; int currentNotificationSize; int currentNotificationIndex; byte currentNotificationType; SendNotificationOperation(ID115Support support, NotificationSpec notificationSpec) { super(support); String phone = ""; if (notificationSpec.phoneNumber != null) { phone = notificationSpec.phoneNumber; } String title = ""; if (notificationSpec.sender != null) { title = notificationSpec.sender; } else if (notificationSpec.title != null) { title = notificationSpec.title; } else if (notificationSpec.subject != null) { title = notificationSpec.subject; } String text = ""; if (notificationSpec.body != null) { text = notificationSpec.body; } currentNotificationBuffer = encodeMessageNotification(notificationSpec.type, title, phone, text); currentNotificationSize = (currentNotificationBuffer.length + 15) / 16; currentNotificationType = ID115Constants.CMD_KEY_NOTIFY_MSG; } SendNotificationOperation(ID115Support support, CallSpec callSpec) { super(support); String number = ""; if (callSpec.number != null) { number = callSpec.number; } String name = ""; if (callSpec.name != null) { name = callSpec.name; } currentNotificationBuffer = encodeCallNotification(name, number); currentNotificationSize = (currentNotificationBuffer.length + 15) / 16; currentNotificationType = ID115Constants.CMD_KEY_NOTIFY_CALL; } @Override boolean isHealthOperation() { return false; } @Override protected void doPerform() throws IOException { sendNotificationChunk(1); } void sendNotificationChunk(int chunkIndex) throws IOException { currentNotificationIndex = chunkIndex; int offset = (chunkIndex - 1) * 16; int tailSize = currentNotificationBuffer.length - offset; int chunkSize = (tailSize > 16)? 16 : tailSize; byte raw[] = new byte[16]; System.arraycopy(currentNotificationBuffer, offset, raw, 0, chunkSize); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(ID115Constants.CMD_ID_NOTIFY); outputStream.write(currentNotificationType); outputStream.write((byte)currentNotificationSize); outputStream.write((byte)currentNotificationIndex); outputStream.write(raw); byte cmd[] = outputStream.toByteArray(); TransactionBuilder builder = performInitialized("send notification chunk"); builder.write(controlCharacteristic, cmd); builder.queue(getQueue()); } void handleResponse(byte[] data) { if (!isOperationRunning()) { LOG.error("ignoring notification because operation is not running. Data length: " + data.length); getSupport().logMessageContent(data); return; } if (data.length < 2) { LOG.warn("short GATT response"); return; } if (data[0] == ID115Constants.CMD_ID_NOTIFY) { if (data.length < 4) { LOG.warn("short GATT response for NOTIFY"); return; } if (data[1] == currentNotificationType) { if (data[3] == currentNotificationIndex) { if (currentNotificationIndex != currentNotificationSize) { try { sendNotificationChunk(currentNotificationIndex + 1); } catch (IOException ex) { GB.toast(getContext(), "Error sending ID115 notification, you may need to connect and disconnect", Toast.LENGTH_LONG, GB.ERROR, ex); } } else { LOG.info("Notification transfer has finished."); operationFinished(); } } } } } byte[] encodeCallNotification(String name, String phone) { if (name.length() > 20) { name = name.substring(0, 20); } if (phone.length() > 20) { phone = phone.substring(0, 20); } byte[] name_bytes = name.getBytes(StandardCharsets.UTF_8); byte[] phone_bytes = phone.getBytes(StandardCharsets.UTF_8); try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write((byte) phone_bytes.length); outputStream.write((byte) name_bytes.length); outputStream.write(phone_bytes); outputStream.write(name_bytes); return outputStream.toByteArray(); } catch (IOException e) { return null; } } byte[] encodeMessageNotification(NotificationType type, String title, String phone, String text) { if (title.length() > 20) { title = title.substring(0, 20); } if (phone.length() > 20) { phone = phone.substring(0, 20); } if (text.length() > 20) { text = text.substring(0, 20); } byte[] title_bytes = title.getBytes(StandardCharsets.UTF_8); byte[] phone_bytes = phone.getBytes(StandardCharsets.UTF_8); byte[] text_bytes = text.getBytes(StandardCharsets.UTF_8); byte nativeType = ID115Constants.getNotificationType(type); try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(nativeType); outputStream.write((byte) text_bytes.length); outputStream.write((byte) phone_bytes.length); outputStream.write((byte) title_bytes.length); outputStream.write(phone_bytes); outputStream.write(title_bytes); outputStream.write(text_bytes); return outputStream.toByteArray(); } catch (IOException e) { return null; } } }