Compare commits

...

34 Commits

Author SHA1 Message Date
Vitaliy Tomin ca026239e8 huawei: Many fixes after pull review
* added missing license headers
* added fileType fields and args, removed hardcoded values
* changed some names from watchface to more general fileupload
* moved app management methods implementation from LE and BR coordinator
  to HuaweiCoordinator
* extra empty lines removed
* removed unnecessary tlv tag checks
2024-04-27 09:41:14 +08:00
Vitaliy Tomin cb82d0c245 huawei: Remove unnecessary RuntimeException throws
* move ui message to translatable string resource
2024-04-27 09:41:12 +08:00
Vitaliy Tomin 57f5658739 huawei: Watchface: Allow watchfaces without preview 2024-04-27 09:39:31 +08:00
Vitaliy Tomin 378ca31bc0 Huawei: fileupload: Move watchface parsing to HuaweiFwHelper
* there was duplicated code in HuaweiUploadManager and HuaweiInstallHandler
* now HuaweiUploadManager does not contain watchface specific info, but
  only information related to general file uploading
* verify if file is could be uploaded befor showing InstallHandler
2024-04-27 09:39:31 +08:00
Vitaliy Tomin c64aeacded fixup! huawei: Implement watchface management 2024-04-27 09:39:31 +08:00
Vitaliy Tomin 88651b98fc huawei: set fileUpload requests addToResponse=false 2024-04-27 09:39:31 +08:00
Vitaliy Tomin c206b8f06f huawei: Implement watchface management
* added watchface management code to HuaweiWatchfaceManager
* HuaweiWatchfaceManager moved from coordinator to support
2024-04-27 09:39:31 +08:00
Vitaliy Tomin ef3654b7e3 huawei: packets and requests to for watchlist
* added packets requests and parsing for getting list of
installed watchfaces and its names
2024-04-27 09:39:31 +08:00
Vitaliy Tomin ad4e373131 huawei: Add watchface uploading progress 2024-04-27 09:39:31 +08:00
Vitaliy Tomin c1f286e823 huawei: set watchface as current on upload complete 2024-04-27 09:39:31 +08:00
Vitaliy Tomin 44a3f2a456 huawei: Implement watchface managing 27 05 2024-04-27 09:39:31 +08:00
Vitaliy Tomin f027d95a33 huawei: fileupload: Refactoring to use file in UploadManager
* generate random file name for watchfaces
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 34e9b5ceb5 huawei: Implemented parser for WatchfaceDeviceParams
* watchface  resolution validation implemented
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 3a8fbbe4d3 huawei: fileuplaod: FileUploadParams refactoring 2024-04-27 09:39:31 +08:00
Vitaliy Tomin 8fd8f2324d huawei: fileupload: Use unitSize for file serialization
* unitSize recieved in 28 04 response tag 5 and means content length
in one 28 06 packet
2024-04-27 09:39:31 +08:00
Vitaliy Tomin cd6bc10239 huawei: watchface: GetWatchfaceParams (27 01)
* request to get supported watchface versions and screen size
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 71389c3fce huawei: fileupload: Fix for magicwatch 2
* magicwatch2 reporing MTU smaller than SliceSize and upload fail
when using slice size
2024-04-27 09:39:31 +08:00
Vitaliy Tomin b543e4717e huawei; fileupload: Parse FileUploadConsultAck.Response 2024-04-27 09:39:31 +08:00
Vitaliy Tomin 68e81a4887 huawei: file upload: SendFileUploadComplete 28 07
* refactoring for AsynchronousResponse - use single method to handle
all FilUpload related packets
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 2d915e7346 Huawei: file upload: Refactoring Watchface -> Fileupload 2024-04-27 09:39:31 +08:00
Vitaliy Tomin c36aa14463 Huawei: file upload: implement file upload packet
* got first working on huawei band 7
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 0229a24dc6 Huawei: watchface upload: fix parse WatchfaceNextChunkParams
* prepare for HuaweiPacket.serializeFileChunk()
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 00af1c6895 Huawei: watchface upload - more request up to chunked upload
* SendWatchfaceHash (28 03)
* SendWatchfaceAck (28 04)
* WatchfaceNextChunkParams packet (28 05)
* SendWatchfaceChunk; (28 06)
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 8e71487092 Huawei: watchface upload - SendWatchfaceInfo
* Added HuaweiWatchfaceManager to store current watchface info
* implemented SendWatchfaceInfo request
* added code to handle onInstallApp for BRCoordinator and LECoordinator
2024-04-27 09:39:31 +08:00
Vitaliy Tomin 054e8e18ee huawei: Initial HuaweiInstallHandler
* able to check if there is watchface bin insize zip
* calc sha256
* show watchface preview
* no real install for now
2024-04-27 09:39:31 +08:00
Damien 'Psolyca' Gaignon c1e0b1fcd5 [Huawei] Enable sleep detection 2024-04-26 19:50:37 +00:00
José Rebelo 408f4b75dd Serbian transliterator: Map Đ and đ 2024-04-25 18:09:25 +01:00
José Rebelo 31408394b4 Serbian transliterator: Map Č and č 2024-04-25 18:08:55 +01:00
José Rebelo 61af26d7ce Add Serbian transliterator
As discussed in #3727
2024-04-25 17:51:45 +01:00
José Rebelo 500e930237 Refactor location service
- Refactor the code from a static global instance to a lifecycle-aware
  service instantiated in the DeviceCommunicationService
- Fix number of devices reported in the notification
- Prevents leaks and properly stops when devices get disconnected
2024-04-25 17:08:53 +01:00
José Rebelo 3799ffb72c Zepp OS: Sync calendar event reminders 2024-04-25 15:58:57 +01:00
José Rebelo 13d6c49bb5 Xiaomi: Sync calendar event reminders 2024-04-25 15:00:48 +01:00
Vitaliy Tomin 67cf9b2f00 huawei: Add huawei account support (#3721)
* this feature allows to pair HarmonyOS devices without factory reset to
  GB and Huawei Health.

* huawei account has form of 17 digit string and could be retrived from
  logcat filtering by huid=

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3721
Co-authored-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
Co-committed-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
2024-04-25 12:19:00 +00:00
Daniele Gobbetti 173e2d29b0 Include Organizer and Reminders when reading calendar events
Also use the named column indexes instead of numeric ids when retrieving the contents to make it more clear and more robust in case further fields are added later.

Reminders are set as absolute timestamp.
2024-04-25 11:46:34 +02:00
60 changed files with 2671 additions and 438 deletions

View File

@ -106,7 +106,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -659,7 +659,7 @@ public class DebugActivity extends AbstractGBActivity {
stopPhoneGpsLocationListener.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBLocationManager.stopAll(getBaseContext());
GBLocationService.stop(DebugActivity.this, null);
}
});

View File

@ -30,8 +30,10 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
@ -83,6 +85,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_ASK;
@ -164,12 +171,31 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
return huaweiCoordinator.getAppManagerActivity();
}
@Override
public boolean supportsAppListFetching() {
return huaweiCoordinator.getSupportsAppListFetching();
}
@Override
public boolean supportsAppsManagement(GBDevice device) {
return false;
return huaweiCoordinator.getSupportsAppsManagement(device);
}
@Override
public boolean supportsWatchfaceManagement(GBDevice device) {
return supportsAppsManagement(device);
}
@Override
public boolean supportsInstalledAppManagement(GBDevice device) {
return huaweiCoordinator.getSupportsInstalledAppManagement(device);
}
@Override
public boolean supportsCachedAppManagement(GBDevice device) {
return huaweiCoordinator.getSupportsCachedAppManagement(device);
}
@Override
@ -202,9 +228,10 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.supportsMusic();
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override

View File

@ -71,6 +71,7 @@ public final class HuaweiConstants {
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
public static final String PREF_HUAWEI_WORKMODE = "workmode";
public static final String PREF_HUAWEI_TRUSLEEP = "trusleep";
public static final String PREF_HUAWEI_ACCOUNT = "huawei_account";
public static final String PREF_HUAWEI_DND_LIFT_WRIST_TYPE = "dnd_lift_wrist_type"; // SharedPref for 0x01 0x1D
public static final String PREF_HUAWEI_DEBUG_REQUEST = "debug_huawei_request";

View File

@ -16,8 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -30,10 +32,13 @@ import org.slf4j.Logger;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications.NotificationConstraintsType;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -49,7 +54,11 @@ public class HuaweiCoordinator {
byte notificationCapabilities = -0x01;
ByteBuffer notificationConstraints = null;
private Watchface.WatchfaceDeviceParams watchfaceDeviceParams;
private final HuaweiCoordinatorSupplier parent;
private boolean transactionCrypted=true;
public HuaweiCoordinator(HuaweiCoordinatorSupplier parent) {
@ -75,6 +84,7 @@ public class HuaweiCoordinator {
}
}
private SharedPreferences getCapabilitiesSharedPreferences() {
return GBApplication.getContext().getSharedPreferences("huawei_coordinator_capatilities" + parent.getDeviceType().name(), Context.MODE_PRIVATE);
}
@ -380,6 +390,8 @@ public class HuaweiCoordinator {
return supportsCommandForService(0x0c, 0x01);
}
public boolean supportsWatchfaceParams(){ return supportsCommandForService(0x27, 0x01);}
public boolean supportsWeather() {
return supportsCommandForService(0x0f, 0x01);
}
@ -540,5 +552,44 @@ public class HuaweiCoordinator {
"zh_CN",
"zh_TW",
};
}
public short getWidth() {
return watchfaceDeviceParams.width;
}
public short getHeight() {
return watchfaceDeviceParams.height;
}
public void setWatchfaceDeviceParams(Watchface.WatchfaceDeviceParams watchfaceDeviceParams) {
this.watchfaceDeviceParams = watchfaceDeviceParams;
}
public Class<? extends Activity> getAppManagerActivity() {
return AppManagerActivity.class;
}
public boolean getSupportsAppListFetching() {
return true;
}
public boolean getSupportsAppsManagement(GBDevice device) {
return true;
}
public boolean getSupportsInstalledAppManagement(GBDevice device) {
return false;
}
public boolean getSupportsCachedAppManagement(GBDevice device) {
return false;
}
public InstallHandler getInstallHandler(Uri uri, Context context) {
HuaweiInstallHandler handler = new HuaweiInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
}

View File

@ -0,0 +1,121 @@
/* Copyright (C) 2024 Vitalii Tomin
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;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFwHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWatchfaceManager;
public class HuaweiInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiInstallHandler.class);
private final Context context;
protected final HuaweiFwHelper helper;
boolean valid = false;
public HuaweiInstallHandler(Uri uri, Context context) {
this.context = context;
this.helper = new HuaweiFwHelper(uri, context);
}
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof HuaweiCoordinatorSupplier)) {
LOG.warn("Coordinator is not a HuaweiCoordinatorSupplier: {}", coordinator.getClass());
installActivity.setInstallEnabled(false);
return;
}
final HuaweiCoordinatorSupplier huaweiCoordinatorSupplier = (HuaweiCoordinatorSupplier) coordinator;
HuaweiWatchfaceManager.WatchfaceDescription description = helper.getWatchfaceDescription();
HuaweiWatchfaceManager.Resolution resolution = new HuaweiWatchfaceManager.Resolution();
String deviceScreen = String.format("%d*%d",huaweiCoordinatorSupplier.getHuaweiCoordinator().getHeight(),
huaweiCoordinatorSupplier.getHuaweiCoordinator().getWidth());
this.valid = resolution.isValid(description.screen, deviceScreen);
installActivity.setInstallEnabled(true);
GenericItem installItem = new GenericItem();
if (helper.getWatchfacePreviewBitmap() != null) {
installItem.setPreview(helper.getWatchfacePreviewBitmap());
}
installItem.setName(description.title);
installActivity.setInstallItem(installItem);
if (device.isBusy()) {
LOG.error("Firmware cannot be installed (device busy)");
installActivity.setInfoText("Firmware cannot be installed (device busy)");
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
if ( !device.isConnected()) {
LOG.error("Firmware cannot be installed (not connected or wrong device)");
installActivity.setInfoText("Firmware cannot be installed (not connected or wrong device)");
installActivity.setInstallEnabled(false);
return;
}
if (!this.valid) {
LOG.error("Watchface cannot be installed");
installActivity.setInfoText(context.getString(R.string.watchface_resolution_doesnt_match,
resolution.screenByThemeVersion(description.screen), deviceScreen));
installActivity.setInstallEnabled(false);
return;
}
//installItem.setDetails(description.version);
installItem.setIcon(R.drawable.ic_watchface);
installActivity.setInfoText(context.getString(R.string.watchface_install_info, installItem.getName(), description.version, description.author));
LOG.debug("Initialized HuaweiInstallHandler");
}
@Override
public boolean isValid() {
return helper.isValid();
}
@Override
public void onStartInstall(GBDevice device) {
helper.unsetFwBytes();
}
}

View File

@ -14,6 +14,7 @@
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;
import android.app.Activity;
@ -30,12 +31,15 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -82,7 +86,12 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
public String[] getSupportedLanguageSettings(GBDevice device) {
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;
@ -164,12 +173,31 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
return huaweiCoordinator.getAppManagerActivity();
}
@Override
public boolean supportsAppListFetching() {
return huaweiCoordinator.getSupportsAppListFetching();
}
@Override
public boolean supportsAppsManagement(GBDevice device) {
return false;
return huaweiCoordinator.getSupportsAppsManagement(device);
}
@Override
public boolean supportsWatchfaceManagement(GBDevice device) {
return supportsAppsManagement(device);
}
@Override
public boolean supportsInstalledAppManagement(GBDevice device) {
return huaweiCoordinator.getSupportsInstalledAppManagement(device);
}
@Override
public boolean supportsCachedAppManagement(GBDevice device) {
return huaweiCoordinator.getSupportsCachedAppManagement(device);
}
@Override
@ -204,7 +232,7 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override

View File

@ -33,6 +33,7 @@ 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.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
@ -40,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
public class HuaweiPacket {
@ -539,6 +541,28 @@ public class HuaweiPacket {
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
case FileUpload.id:
switch(this.commandId) {
case FileUpload.FileNextChunkParams.id:
return new FileUpload.FileNextChunkParams(paramsProvider).fromPacket(this);
case FileUpload.FileUploadConsultAck.id:
return new FileUpload.FileUploadConsultAck.Response(paramsProvider).fromPacket(this);
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
case Watchface.id:
switch (this.commandId) {
case Watchface.WatchfaceParams.id:
return new Watchface.WatchfaceParams.Response(paramsProvider).fromPacket(this);
case Watchface.DeviceWatchInfo.id:
return new Watchface.DeviceWatchInfo.Response(paramsProvider).fromPacket(this);
case Watchface.WatchfaceNameInfo.id:
return new Watchface.WatchfaceNameInfo.Response(paramsProvider).fromPacket(this);
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
@ -662,6 +686,62 @@ public class HuaweiPacket {
return retv;
}
public List<byte[]> serializeFileChunk(byte[] fileChunk, int uploadPosition, short unitSize) {
List<byte[]> retv = new ArrayList<>();
int headerLength = 5; // Magic + (short)(bodyLength + 1) + 0x00
int sliceHeaderLenght =7;
int footerLength = 2; //CRC16
int packetCount = (int) Math.ceil(((double) fileChunk.length ) / (double) unitSize);
ByteBuffer buffer = ByteBuffer.wrap(fileChunk);
byte fileType = 0x01; //TODO: 1 - watchface, 2 - music
int sliceStart = uploadPosition;
for (int i = 0; i < packetCount; i++) {
short contentSize = (short) Math.min(unitSize, buffer.remaining());
short packetSize = (short)(contentSize + headerLength + sliceHeaderLenght + footerLength);
ByteBuffer packet = ByteBuffer.allocate(packetSize);
int start = packet.position();
packet.put((byte) 0x5a); // Magic byte
packet.putShort((short) (packetSize - headerLength)); // Length
packet.put((byte) 0x00);
packet.put(this.serviceId);
packet.put(this.commandId);
packet.put(fileType); // Slice
packet.put((byte)i); // Flag
packet.putInt(sliceStart);
byte[] packetContent = new byte[contentSize];
buffer.get(packetContent);
packet.put(packetContent); // Packet databyte[] packetContent = new byte[contentSize];
int length = packet.position() - start;
if (length != packetSize - footerLength) {
// TODO: exception?
LOG.error(String.format(GBApplication.getLanguage(), "Packet lengths don't match! %d != %d", length, packetSize + headerLength));
}
byte[] complete = new byte[length];
packet.position(start);
packet.get(complete, 0, length);
int crc16 = CheckSums.getCRC16(complete, 0x0000);
packet.putShort((short) crc16); // CRC16
sliceStart += contentSize;
retv.add(packet.array());
}
return retv;
}
public List<byte[]> serialize() throws CryptoException {
// TODO: necessary for this to work:
// - serviceId

View File

@ -26,15 +26,18 @@ public class AccountRelated {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider) {
public Request (ParamsProvider paramsProvider, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01);
}
this.complete = true;
}
}
@ -50,14 +53,19 @@ public class AccountRelated {
public static final byte id = 0x05;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization) {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, (byte)0x00);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01, (byte)0x00);
}
if (accountPairingOptimization) {
this.tlv.put(0x03, (byte)0x01);
}

View File

@ -0,0 +1,202 @@
/* Copyright (C) 2024 Vitalii Tomin
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 FileUpload {
public static final byte id = 0x28;
public static class FileUploadParams {
public byte file_id = 0;
public String protocolVersion = "";
public short app_wait_time = 0;
public byte bitmap_enable = 0;
public short unit_size = 0;
public int max_apply_data_size = 0;
public short interval =0;
public int received_file_size =0;
public byte no_encrypt = 0;
}
public static class Filetype {
public static final byte watchface = 1;
public static final byte music = 2;
public static final byte backgroundImage = 3;
public static final byte app = 7;
}
public static class FileInfoSend {
public static final byte id = 0x02;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
int fileSize,
String fileName,
byte fileType) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
String watchfaceName = fileName.split("_")[0];
String watchfaceVersion = fileName.split("_")[1];
this.tlv = new HuaweiTLV()
.put(0x01, fileName)
.put(0x02, fileSize)
.put(0x03, (byte) fileType);
if (fileType == Filetype.watchface)
this.tlv.put(0x05, watchfaceName)
.put(0x06, watchfaceVersion);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
}
}
public static class FileHashSend {
public static final byte id = 0x03;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
byte[] hash,
byte fileType) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, fileType)
.put(0x03, hash);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
}
}
public static class FileUploadConsultAck {
public static final byte id = 0x04;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte noEncryption, byte fileType) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x7f, 0x000186A0) //ok
.put(0x01, fileType);
if (noEncryption == 1)
this.tlv.put(0x09, (byte)0x01); // need on devices which generally encrypted, but files
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public FileUploadParams fileUploadParams = new FileUploadParams();
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws HuaweiPacket.ParseException {
this.fileUploadParams.file_id = this.tlv.getByte(0x01);
this.fileUploadParams.protocolVersion = this.tlv.getString(0x02);
this.fileUploadParams.app_wait_time = this.tlv.getShort(0x03);
this.fileUploadParams.bitmap_enable = this.tlv.getByte(0x04);
this.fileUploadParams.unit_size = this.tlv.getShort(0x05);
this.fileUploadParams.max_apply_data_size = this.tlv.getInteger(0x06);
this.fileUploadParams.interval = this.tlv.getShort(0x07);
this.fileUploadParams.received_file_size = this.tlv.getInteger(0x08);
if (this.tlv.contains(0x09)) // optional for older devices
this.fileUploadParams.no_encrypt = this.tlv.getByte(0x09);
}
}
}
public static class FileNextChunkParams extends HuaweiPacket {
public static final byte id = 0x05;
public int bytesUploaded = 0;
public int nextchunkSize = 0;
public FileNextChunkParams(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
this.complete = true;
}
@Override
public void parseTlv() throws HuaweiPacket.ParseException {
this.bytesUploaded = this.tlv.getInteger(0x02);
this.nextchunkSize = this.tlv.getInteger(0x03);
}
}
public static class FileNextChunkSend extends HuaweiPacket {
public static final byte id = 0x06;
public FileNextChunkSend(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
this.complete = true;
}
}
public static class FileUploadResult {
public static final byte id = 0x07;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte fileType) {
super(paramsProvider);
this.serviceId = FileUpload.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x7f, 0x000186A0) //ok
.put(0x01, fileType);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
byte status = 0;
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws HuaweiPacket.ParseException {
this.status = this.tlv.getByte(0x02);
}
}
}
}

View File

@ -0,0 +1,247 @@
/* Copyright (C) 2024 Vitalii Tomin
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class Watchface {
public static final byte id = 0x27;
public static class WatchfaceDeviceParams {
public String maxVersion = "";
public short width = 0;
public short height = 0;
public byte supportFileType = 1;
public byte sort = 1;
public String otherWatchfaceVersions = "";
}
public static class InstalledWatchfaceInfo {
public String fileName = "";
public String version = "";
public byte type = 0;
// bit 0 - is current
// bit 1 - is factory preset
// bit 2 - ???
// bit 3 - editable
// bit 4 - video
// bit 5 - photo
// bit 6 - tryout (trial version)
// bit 7 - kaleidoskop
public byte expandedtype = 0;
public InstalledWatchfaceInfo(HuaweiTLV tlv) throws HuaweiPacket.MissingTagException {
this.fileName = tlv.getString(0x03);
this.version = tlv.getString(0x04);
this.type = tlv.getByte(0x05);
if (tlv.contains(0x07)) // optional
this.expandedtype = tlv.getByte(0x07);
}
public boolean isCurrent() {
return (this.type & 1) == 1;
}
public boolean isFactory() {
return ((this.type >> 1 )& 1) == 1;
}
public boolean isEditable() {
return ((this.type >> 3 )& 1) == 1;
}
public boolean isVideo() {
return ((this.type >> 4 )& 1) == 1;
}
public boolean isPhoto() {
return ((this.type >> 5 )& 1) == 1;
}
public boolean isTryout() {
return ((this.type >> 6 )& 1) == 1;
}
public boolean isKaleidoskop() {
return ((this.type >> 7 )& 1) == 1;
}
}
public static class WatchfaceParams {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Watchface.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01)
.put(0x02)
.put(0x03)
.put(0x04)
.put(0x05)
.put(0x0e)
.put(0x0f);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public WatchfaceDeviceParams params = new WatchfaceDeviceParams();
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
this.params.maxVersion = this.tlv.getString(0x01);
this.params.width = this.tlv.getShort(0x02);
this.params.height = this.tlv.getShort(0x03);
this.params.supportFileType = this.tlv.getByte(0x04);
this.params.sort = this.tlv.getByte(0x05);
this.params.otherWatchfaceVersions = this.tlv.getString(0x06);
}
}
}
public static class DeviceWatchInfo {
public static final byte id = 0x02;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Watchface.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01)
.put(0x06, (byte) 0x03); //3 -overseas non-test, 2 - test, 1 -null?
}
}
public static class Response extends HuaweiPacket {
public List<InstalledWatchfaceInfo> watchfaceInfoList;
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws HuaweiPacket.ParseException {
watchfaceInfoList = new ArrayList<>();
if(this.tlv.contains(0x81)) {
for (HuaweiTLV subTlv : this.tlv.getObject(0x81).getObjects(0x82)) {
watchfaceInfoList.add(new Watchface.InstalledWatchfaceInfo(subTlv));
}
}
}
}
}
public static class WatchfaceOperation {
public static final byte id = 0x03;
public static final byte operationActive = 1;
public static final byte operationDelete = 2;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
String fileName, byte operation) {
super(paramsProvider);
this.serviceId = Watchface.id;
this.tlv = new HuaweiTLV()
.put(0x01, fileName.split("_")[0])
.put(0x02, fileName.split("_")[1])
.put(0x03, operation);
this.commandId = id;
}
}
public static class Response extends HuaweiPacket {
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
}
}
public static class WatchfaceConfirm {
public static final byte id = 0x05;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
String fileName) {
super(paramsProvider);
this.serviceId = Watchface.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, fileName.split("_")[0])
.put(0x02, fileName.split("_")[1])
.put(0x7f, 0x000186A0);
}
}
public static class Response extends HuaweiPacket {
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
}
}
public static class WatchfaceNameInfo {
public static final byte id = 0x06;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
List<InstalledWatchfaceInfo> watchfaceList) {
super(paramsProvider);
this.serviceId = Watchface.id;
this.commandId = id;
HuaweiTLV tlvList = new HuaweiTLV();
for (InstalledWatchfaceInfo watchface : watchfaceList) {
//TODO: ask name only for custom watchfaces
HuaweiTLV wfTlv = new HuaweiTLV().put(0x04, watchface.fileName);
tlvList.put(0x83, wfTlv);
}
this.tlv = new HuaweiTLV()
.put(0x01, (byte) 0x01)
.put(0x82, tlvList);
}
}
public static class Response extends HuaweiPacket {
public HashMap<String, String> watchFaceNames = new HashMap<String, String>();
public Response (ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws HuaweiPacket.ParseException {
if(this.tlv.contains(0x82)) {
for (HuaweiTLV subTlv : this.tlv.getObject(0x82).getObjects(0x83)) {
watchFaceNames.put(subTlv.getString(0x04), subTlv.getString(0x05));
}
}
}
}
}
}

View File

@ -26,6 +26,7 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.GregorianCalendar;
@ -188,6 +189,7 @@ public class CalendarReceiver extends BroadcastReceiver {
calendarEventSpec.id = i;
calendarEventSpec.title = calendarEvent.getTitle();
calendarEventSpec.allDay = calendarEvent.isAllDay();
calendarEventSpec.reminders = new ArrayList<>(calendarEvent.getRemindersAbsoluteTs());
calendarEventSpec.timestamp = calendarEvent.getBeginSeconds();
calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now
if (calendarEvent.isAllDay()) {

View File

@ -21,10 +21,14 @@ import android.location.LocationListener;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* An implementation of a {@link LocationListener} that forwards the location updates to the
@ -33,18 +37,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
public class GBLocationListener implements LocationListener {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationListener.class);
private final EventHandler eventHandler;
private final GBDevice device;
private Location previousLocation;
// divide by 3.6 to get km/h to m/s
private static final double SPEED_THRESHOLD = 1.0 / 3.6;
public GBLocationListener(final EventHandler eventHandler) {
this.eventHandler = eventHandler;
public GBLocationListener(final GBDevice device) {
this.device = device;
}
@Override
public void onLocationChanged(final Location location) {
public void onLocationChanged(@NonNull final Location location) {
LOG.info("Location changed: {}", location);
// Correct the location time
@ -61,16 +65,16 @@ public class GBLocationListener implements LocationListener {
previousLocation = location;
eventHandler.onSetGpsLocation(location);
GBApplication.deviceService(device).onSetGpsLocation(location);
}
@Override
public void onProviderDisabled(final String provider) {
public void onProviderDisabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}
@Override
public void onProviderEnabled(final String provider) {
public void onProviderEnabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}

View File

@ -1,140 +0,0 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider running.
*/
public class GBLocationManager {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationManager.class);
/**
* The current number of running listeners.
*/
private static Map<EventHandler, Map<LocationProviderType, AbstractLocationProvider>> providers = new HashMap<>();
public static void start(final Context context, final EventHandler eventHandler) {
GBLocationManager.start(context, eventHandler, LocationProviderType.GPS, null);
}
public static void start(final Context context, final EventHandler eventHandler, final LocationProviderType providerType, Integer updateInterval) {
LOG.info("Starting");
if (providers.containsKey(eventHandler) && providers.get(eventHandler).containsKey(providerType)) {
LOG.warn("EventHandler already registered");
return;
}
GB.createGpsNotification(context, providers.size());
final GBLocationListener locationListener = new GBLocationListener(eventHandler);
final AbstractLocationProvider locationProvider;
switch (providerType) {
case GPS:
LOG.info("Using gps location provider");
locationProvider = new PhoneGpsLocationProvider(locationListener);
break;
case NETWORK:
LOG.info("Using network location provider");
locationProvider = new PhoneNetworkLocationProvider(locationListener);
break;
default:
LOG.info("Using default location provider: GPS");
locationProvider = new PhoneGpsLocationProvider(locationListener);
}
if (updateInterval != null) {
locationProvider.start(context, updateInterval);
} else {
locationProvider.start(context);
}
if (providers.containsKey(eventHandler)) {
providers.get(eventHandler).put(providerType, locationProvider);
} else {
Map<LocationProviderType, AbstractLocationProvider> providerMap = new HashMap<>();
providerMap.put(providerType, locationProvider);
providers.put(eventHandler, providerMap);
}
}
public static void stop(final Context context, final EventHandler eventHandler) {
GBLocationManager.stop(context, eventHandler, null);
}
public static void stop(final Context context, final EventHandler eventHandler, final LocationProviderType gpsType) {
if (!providers.containsKey(eventHandler)) return;
Map<LocationProviderType, AbstractLocationProvider> providerMap = providers.get(eventHandler);
if (gpsType == null) {
Set<LocationProviderType> toBeRemoved = new HashSet<>();
for (LocationProviderType providerType: providerMap.keySet()) {
stopProvider(context, providerMap.get(providerType));
toBeRemoved.add(providerType);
}
for (final LocationProviderType providerType : toBeRemoved) {
providerMap.remove(providerType);
}
} else {
stopProvider(context, providerMap.get(gpsType));
providerMap.remove(gpsType);
}
LOG.debug("Remaining providers: " + providers.size());
if (providers.get(eventHandler).size() == 0)
providers.remove(eventHandler);
updateNotification(context);
}
private static void updateNotification(final Context context){
if (!providers.isEmpty()) {
GB.createGpsNotification(context, providers.size());
} else {
GB.removeGpsNotification(context);
}
}
private static void stopProvider(final Context context, AbstractLocationProvider locationProvider) {
if (locationProvider != null) {
locationProvider.stop(context);
}
}
public static void stopAll(final Context context) {
for (EventHandler eventHandler : providers.keySet()) {
stop(context, eventHandler);
}
}
}

View File

@ -22,35 +22,30 @@ import android.location.LocationListener;
/**
* An abstract location provider, which periodically sends a location update to the provided {@link LocationListener}.
*/
public abstract class AbstractLocationProvider {
public abstract class GBLocationProvider {
private final Context context;
private final LocationListener locationListener;
public AbstractLocationProvider(final LocationListener locationListener) {
public GBLocationProvider(final Context context, final LocationListener locationListener) {
this.context = context;
this.locationListener = locationListener;
}
protected final LocationListener getLocationListener() {
public final Context getContext() {
return this.context;
}
public final LocationListener getLocationListener() {
return this.locationListener;
}
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context);
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context, final int interval);
public abstract void start(final int interval);
/**
* Stop sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void stop(final Context context);
public abstract void stop();
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2022-2024 LukasEdl
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.externalevents.gps;
import android.content.Context;
import android.location.LocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.MockLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.PhoneLocationProvider;
public enum GBLocationProviderType {
GPS {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.GPS_PROVIDER);
}
},
NETWORK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.NETWORK_PROVIDER);
}
},
MOCK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new MockLocationProvider(context, locationListener);
}
},
;
public abstract GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener);
}

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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.externalevents.gps;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider running.
*/
public class GBLocationService extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationService.class);
public static final String ACTION_START = "GBLocationService.START";
public static final String ACTION_STOP = "GBLocationService.STOP";
public static final String ACTION_STOP_ALL = "GBLocationService.STOP_ALL";
public static final String EXTRA_TYPE = "extra_type";
public static final String EXTRA_INTERVAL = "extra_interval";
private final Context context;
private final Map<GBDevice, List<GBLocationProvider>> providersByDevice = new HashMap<>();
public GBLocationService(final Context context) {
this.context = context;
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction() == null) {
LOG.warn("Action is null");
return;
}
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
switch (intent.getAction()) {
case ACTION_START:
if (device == null) {
LOG.error("Device is null for {}", intent.getAction());
return;
}
final GBLocationProviderType providerType = GBLocationProviderType.valueOf(
intent.hasExtra(EXTRA_TYPE) ? intent.getStringExtra(EXTRA_TYPE) : "GPS"
);
final int updateInterval = intent.getIntExtra(EXTRA_INTERVAL, 1000);
LOG.debug("Starting location provider {} for {}", providerType, device.getAliasOrName());
if (!providersByDevice.containsKey(device)) {
providersByDevice.put(device, new ArrayList<>());
}
updateNotification();
final List<GBLocationProvider> existingProviders = providersByDevice.get(device);
final GBLocationListener locationListener = new GBLocationListener(device);
final GBLocationProvider locationProvider = providerType.newInstance(context, locationListener);
locationProvider.start(updateInterval);
Objects.requireNonNull(existingProviders).add(locationProvider);
return;
case ACTION_STOP:
if (device != null) {
stopDevice(device);
updateNotification();
} else {
stopAll();
}
return;
case ACTION_STOP_ALL:
stopAll();
return;
default:
LOG.warn("Unknown action {}", intent.getAction());
}
}
public void stopDevice(final GBDevice device) {
LOG.debug("Stopping location providers for {}", device.getAliasOrName());
final List<GBLocationProvider> providers = providersByDevice.remove(device);
if (providers != null) {
for (final GBLocationProvider provider : providers) {
provider.stop();
}
}
}
public IntentFilter buildFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_START);
intentFilter.addAction(ACTION_STOP);
return intentFilter;
}
public void stopAll() {
LOG.info("Stopping location service for all devices");
final List<GBDevice> gbDevices = new ArrayList<>(providersByDevice.keySet());
for (GBDevice d : gbDevices) {
stopDevice(d);
}
updateNotification();
}
public static void start(final Context context,
@NonNull final GBDevice device,
final GBLocationProviderType providerType,
final int updateInterval) {
final Intent intent = new Intent(ACTION_START);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
intent.putExtra(EXTRA_TYPE, providerType.name());
intent.putExtra(EXTRA_INTERVAL, updateInterval);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static void stop(final Context context, @Nullable final GBDevice device) {
final Intent intent = new Intent(ACTION_STOP);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
private void updateNotification() {
if (!providersByDevice.isEmpty()) {
final Intent notificationIntent = new Intent(context, GBLocationService.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
final NotificationCompat.Builder nb = new NotificationCompat.Builder(context, GB.NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, providersByDevice.size()))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
GB.notify(GB.NOTIFICATION_ID_GPS, nb.build(), context);
} else {
GB.removeNotification(GB.NOTIFICATION_ID_GPS, context);
}
}
}

View File

@ -1,22 +0,0 @@
/* Copyright (C) 2022-2024 LukasEdl
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.externalevents.gps;
public enum LocationProviderType {
GPS,
NETWORK,
}

View File

@ -1,80 +0,0 @@
/* Copyright (C) 2022-2024 Lukas, LukasEdl
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.externalevents.gps;
import android.Manifest;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneNetworkLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneNetworkLocationProvider.class);
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneNetworkLocationProvider(LocationListener locationListener) {
super(locationListener);
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
LOG.info("Starting phone network location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
interval,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
);
final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
LOG.debug("Last known network location: {}", lastKnownLocation);
}
@Override
void stop(final Context context) {
LOG.info("Stopping phone network location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -14,7 +14,7 @@
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.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.content.Context;
import android.location.Location;
@ -26,13 +26,14 @@ import android.os.SystemClock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
/**
* A mock location provider which keeps updating the location at a constant speed, starting from the
* last known location. Useful for local tests.
*/
public class MockLocationProvider extends AbstractLocationProvider {
public class MockLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(MockLocationProvider.class);
private Location previousLocation = new CurrentPosition().getLastKnownLocation();
@ -40,12 +41,12 @@ public class MockLocationProvider extends AbstractLocationProvider {
/**
* Interval between location updates, in milliseconds.
*/
private final int interval = 1000;
private static final int DEFAULT_INTERVAL = 1000;
/**
* Difference between location updates, in degrees.
*/
private final float coordDiff = 0.0002f;
private static final float COORD_DIFF = 0.0002f;
/**
* Whether the handler is running.
@ -54,50 +55,40 @@ public class MockLocationProvider extends AbstractLocationProvider {
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable locationUpdateRunnable = new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + coordDiff);
newLocation.setTime(System.currentTimeMillis());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
};
public MockLocationProvider(LocationListener locationListener) {
super(locationListener);
public MockLocationProvider(final Context context, final LocationListener locationListener) {
super(context, locationListener);
}
@Override
void start(final Context context) {
public void start(final int interval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + COORD_DIFF);
newLocation.setTime(System.currentTimeMillis());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
}, interval > 0 ? interval : DEFAULT_INTERVAL);
}
@Override
void start(final Context context, int minInterval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping mock location provider");
running = false;

View File

@ -14,7 +14,7 @@
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.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.Manifest;
import android.content.Context;
@ -27,43 +27,38 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneGpsLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneGpsLocationProvider.class);
public class PhoneLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneLocationProvider.class);
private final String provider;
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneGpsLocationProvider(LocationListener locationListener) {
super(locationListener);
}
public PhoneGpsLocationProvider(LocationListener locationListener, int intervalTime) {
super(locationListener);
public PhoneLocationProvider(final Context context, final LocationListener locationListener, final String provider) {
super(context, locationListener);
this.provider = provider;
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
public void start(final int interval) {
LOG.info("Starting phone gps location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (!GB.checkPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
interval,
provider,
interval > 0 ? interval : 1_000,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
@ -74,10 +69,10 @@ public class PhoneGpsLocationProvider extends AbstractLocationProvider {
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping phone gps location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -451,6 +451,7 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_CALENDAREVENT_TIMESTAMP, calendarEventSpec.timestamp)
.putExtra(EXTRA_CALENDAREVENT_DURATION, calendarEventSpec.durationInSeconds)
.putExtra(EXTRA_CALENDAREVENT_ALLDAY, calendarEventSpec.allDay)
.putExtra(EXTRA_CALENDAREVENT_REMINDERS, calendarEventSpec.reminders)
.putExtra(EXTRA_CALENDAREVENT_TITLE, calendarEventSpec.title)
.putExtra(EXTRA_CALENDAREVENT_DESCRIPTION, calendarEventSpec.description)
.putExtra(EXTRA_CALENDAREVENT_CALNAME, calendarEventSpec.calName)

View File

@ -17,6 +17,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.ArrayList;
public class CalendarEventSpec {
public static final byte TYPE_UNKNOWN = 0;
public static final byte TYPE_SUNRISE = 1;
@ -32,4 +34,5 @@ public class CalendarEventSpec {
public String calName;
public int color;
public boolean allDay;
public ArrayList<Long> reminders; // unix epoch millis
}

View File

@ -162,6 +162,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_TIMESTAMP = "calendarevent_timestamp";
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_ALLDAY = "calendarevent_allday";
String EXTRA_CALENDAREVENT_REMINDERS = "calendarevent_reminders";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
String EXTRA_CALENDAREVENT_LOCATION = "calendarevent_location";

View File

@ -82,7 +82,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.SilentModeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
@ -140,7 +140,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
}
private class FeatureSet{
private static class FeatureSet {
private boolean supportsWeather = false;
private boolean supportsActivityDataFetching = false;
private boolean supportsCalendarEvents = false;
@ -256,7 +256,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private AutoConnectIntervalReceiver mAutoConnectInvervalReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private final List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private CMWeatherReceiver mCMWeatherReceiver = null;
private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null;
private TinyWeatherForecastGermanyReceiver mTinyWeatherForecastGermanyReceiver = null;
@ -264,6 +264,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private OmniJawsObserver mOmniJawsObserver = null;
private final DeviceSettingsReceiver deviceSettingsReceiver = new DeviceSettingsReceiver();
private final IntentApiReceiver intentApiReceiver = new IntentApiReceiver();
private GBLocationService locationService = null;
private OsmandEventReceiver mOsmandAidlHelper = null;
@ -860,6 +861,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
calendarEventSpec.timestamp = intent.getIntExtra(EXTRA_CALENDAREVENT_TIMESTAMP, -1);
calendarEventSpec.durationInSeconds = intent.getIntExtra(EXTRA_CALENDAREVENT_DURATION, -1);
calendarEventSpec.allDay = intent.getBooleanExtra(EXTRA_CALENDAREVENT_ALLDAY, false);
calendarEventSpec.reminders = (ArrayList<Long>) intent.getSerializableExtra(EXTRA_CALENDAREVENT_REMINDERS);
calendarEventSpec.title = intent.getStringExtra(EXTRA_CALENDAREVENT_TITLE);
calendarEventSpec.description = intent.getStringExtra(EXTRA_CALENDAREVENT_DESCRIPTION);
calendarEventSpec.location = intent.getStringExtra(EXTRA_CALENDAREVENT_LOCATION);
@ -1342,6 +1344,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
registerReceiver(mSilentModeReceiver, filter);
}
if (locationService == null) {
locationService = new GBLocationService(this);
LocalBroadcastManager.getInstance(this).registerReceiver(locationService, locationService.buildFilter());
}
if (mOsmandAidlHelper == null && features.supportsNavigation()) {
mOsmandAidlHelper = new OsmandEventReceiver(this.getApplication());
}
@ -1424,6 +1431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mSilentModeReceiver);
mSilentModeReceiver = null;
}
if (locationService != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(locationService);
locationService.stopAll();
locationService = null;
}
if (mCMWeatherReceiver != null) {
unregisterReceiver(mCMWeatherReceiver);
mCMWeatherReceiver = null;

View File

@ -120,8 +120,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncStateDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.LocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
@ -215,7 +215,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
if (!gpsUpdateSetup)
return;
LOG.info("Stop location updates");
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
gpsUpdateSetup = false;
}
@ -1140,14 +1140,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Using combined GPS and NETWORK based location: " + onlyUseNetworkGPS);
if (!onlyUseNetworkGPS) {
try {
GBLocationManager.start(getContext(), this, LocationProviderType.GPS, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("GPS provider could not be started", e);
}
}
try {
GBLocationManager.start(getContext(), this, LocationProviderType.NETWORK, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.NETWORK, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("NETWORK provider could not be started", e);
}

View File

@ -30,7 +30,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
@ -117,7 +116,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
@ -2010,7 +2010,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
if (sendGpsToBand) {
lastPhoneGpsSent = 0;
sendPhoneGps(HuamiPhoneGpsStatus.SEARCHING, null);
GBLocationManager.start(getContext(), this);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else {
sendPhoneGps(HuamiPhoneGpsStatus.DISABLED, null);
}
@ -2030,7 +2030,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
protected void onWorkoutEnd() {
final boolean startOnPhone = HuamiCoordinator.getWorkoutStartOnPhone(getDevice().getAddress());
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
if (startOnPhone) {
LOG.info("Stopping OpenTracks recording");

View File

@ -143,10 +143,12 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
buf.putInt(calendarEventSpec.timestamp + calendarEventSpec.durationInSeconds);
// Remind
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
if (calendarEventSpec.reminders != null && !calendarEventSpec.reminders.isEmpty()) {
buf.putInt((int) (calendarEventSpec.reminders.get(0) / 1000L));
} else {
buf.putInt(0);
}
// Repeat
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
@ -231,7 +233,10 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
final int endTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 00 00 00 00 ff ff ff ff
final int reminderTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 ff ff ff ff
i += 12;
boolean allDay = (payload[i] == 0x01);

View File

@ -48,12 +48,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationListener;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadComplete;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadAck;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadChunk;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadHash;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceConfirm;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -102,6 +108,8 @@ public class AsynchronousResponse {
handleMenstrualModifyTime(response);
handleWeatherCheck(response);
handleGpsRequest(response);
handleFileUpload(response);
handleWatchface(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
@ -386,6 +394,77 @@ public class AsynchronousResponse {
}
}
private void handleFileUpload(HuaweiPacket response) throws Request.ResponseParseException {
if (response.serviceId == FileUpload.id) {
if (response.commandId == FileUpload.FileHashSend.id) {
try {
SendFileUploadHash sendFileUploadHash = new SendFileUploadHash(support, support.huaweiUploadManager);
sendFileUploadHash.doPerform();
} catch (IOException e) {
LOG.error("Could not send fileupload hash request", e);
}
} else if (response.commandId == FileUpload.FileUploadConsultAck.id) {
if (!(response instanceof FileUpload.FileUploadConsultAck.Response))
throw new Request.ResponseTypeMismatchException(response, FileUpload.FileUploadConsultAck.Response.class);
FileUpload.FileUploadConsultAck.Response resp = (FileUpload.FileUploadConsultAck.Response) response;
support.huaweiUploadManager.setFileUploadParams(resp.fileUploadParams);
try {
support.huaweiUploadManager.setDeviceBusy();
SendFileUploadAck sendFileUploadAck = new SendFileUploadAck(support,
resp.fileUploadParams.no_encrypt, support.huaweiUploadManager.getFileType());
sendFileUploadAck.doPerform();
} catch (IOException e) {
LOG.error("Could not send fileupload ack request", e);
}
} else if (response.commandId == FileUpload.FileNextChunkParams.id) {
if (!(response instanceof FileUpload.FileNextChunkParams))
throw new Request.ResponseTypeMismatchException(response, FileUpload.FileNextChunkParams.class);
FileUpload.FileNextChunkParams resp = (FileUpload.FileNextChunkParams) response;
support.huaweiUploadManager.setUploadChunkSize(resp.nextchunkSize);
support.huaweiUploadManager.setCurrentUploadPosition(resp.bytesUploaded);
int progress = Math.round(((float)resp.bytesUploaded / (float)support.huaweiUploadManager.getFileSize())* 100);
support.onUploadProgress(R.string.updatefirmwareoperation_update_in_progress, progress, true);
try {
SendFileUploadChunk sendFileUploadChunk = new SendFileUploadChunk(support, support.huaweiUploadManager);
sendFileUploadChunk.doPerform();
} catch (IOException e) {
LOG.error("Could not send fileupload next chunk request", e);
}
} else if (response.commandId == FileUpload.FileUploadResult.id) {
try {
support.huaweiUploadManager.unsetDeviceBusy();
support.onUploadProgress(R.string.updatefirmwareoperation_update_complete, 100, false);
SendFileUploadComplete sendFileUploadComplete = new SendFileUploadComplete(this.support, this.support.huaweiUploadManager.getFileType());
SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(this.support, this.support.huaweiUploadManager.getFileName(), Watchface.WatchfaceOperation.operationActive);
sendFileUploadComplete.doPerform();
sendWatchfaceOperation.doPerform();
} catch (IOException e) {
LOG.error("Could not send fileupload result request", e);
}
}
}
}
private void handleWatchface(HuaweiPacket response) throws Request.ResponseParseException {
if (response.serviceId == Watchface.id) {
if (response.commandId == Watchface.WatchfaceConfirm.id) {
try {
SendWatchfaceConfirm sendWatchfaceConfirm = new SendWatchfaceConfirm(this.support, this.support.huaweiUploadManager.getFileName());
sendWatchfaceConfirm.doPerform();
} catch (IOException e) {
LOG.error("Could not send watchface confirm request", e);
}
}
}
}
private void handleWeatherCheck(HuaweiPacket response) {
if (response.serviceId == Weather.id && response.commandId == 0x04) {
// Send back ok

View File

@ -17,10 +17,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.location.Location;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -130,4 +132,27 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
public void onSetGpsLocation(Location location) {
supportProvider.onSetGpsLocation(location);
}
@Override
public void onInstallApp(Uri uri) {
supportProvider.onInstallApp(uri);
}
@Override
public void onAppInfoReq() {
supportProvider.onAppInfoReq();
}
@Override
public void onAppStart(final UUID uuid, boolean start) {
if (start) {
supportProvider.onAppStart(uuid, start);
}
}
@Override
public void onAppDelete(final UUID uuid) {
supportProvider.onAppDelete(uuid);
}
}

View File

@ -0,0 +1,139 @@
/* Copyright (C) 2024 Vitalii Tomin
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;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
public class HuaweiFwHelper {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiFwHelper.class);
private final Uri uri;
private byte[] fw;
private int fileSize = 0;
private boolean typeWatchface;
private byte fileType = 0;
String fileName = "";
Bitmap watchfacePreviewBitmap;
HuaweiWatchfaceManager.WatchfaceDescription watchfaceDescription;
Context mContext;
public HuaweiFwHelper(final Uri uri, final Context context) {
this.uri = uri;
final UriHelper uriHelper;
this.mContext = context;
try {
uriHelper = UriHelper.get(uri, context);
} catch (final IOException e) {
LOG.error("Failed to get uri helper for {}", uri, e);
return;
}
parseFile();
}
private void parseFile() {
if (parseAsWatchFace()) {
assert watchfaceDescription.screen != null;
assert watchfaceDescription.title != null;
typeWatchface = true;
fileType = FileUpload.Filetype.watchface;
}
}
public byte[] getBytes() {
return fw;
}
public void unsetFwBytes() {
this.fw = null;
}
boolean parseAsWatchFace() {
try {
final UriHelper uriHelper = UriHelper.get(uri, this.mContext);
GBZipFile watchfacePackage = new GBZipFile(uriHelper.openInputStream());
String xmlDescription = new String(watchfacePackage.getFileFromZip("description.xml"));
watchfaceDescription = new HuaweiWatchfaceManager.WatchfaceDescription(xmlDescription);
if (watchfacePackage.fileExists("preview/cover.jpg")) {
final byte[] preview = watchfacePackage.getFileFromZip("preview/cover.jpg");
watchfacePreviewBitmap = BitmapFactory.decodeByteArray(preview, 0, preview.length);
}
fw = watchfacePackage.getFileFromZip("com.huawei.watchface");
fileSize = fw.length;
} catch (ZipFileException e) {
LOG.error("Unable to read watchface file.", e);
return false;
} catch (FileNotFoundException e) {
LOG.error("The watchface file was not found.", e);
return false;
} catch (IOException e) {
LOG.error("General IO error occurred.", e);
return false;
} catch (Exception e) {
LOG.error("Unknown error occurred.", e);
return false;
}
return true;
}
public boolean isWatchface() {
return typeWatchface;
}
public boolean isValid() {
return isWatchface();
}
public Bitmap getWatchfacePreviewBitmap() {
return watchfacePreviewBitmap;
}
public HuaweiWatchfaceManager.WatchfaceDescription getWatchfaceDescription() {
return watchfaceDescription;
}
public byte getFileType() {
return fileType;
}
public String getFileName() {
return fileName;
}
}

View File

@ -19,11 +19,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.location.Location;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -137,4 +140,28 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
public void onSetGpsLocation(Location location) {
supportProvider.onSetGpsLocation(location);
}
@Override
public void onInstallApp(Uri uri) {
supportProvider.onInstallApp(uri);
}
@Override
public void onAppInfoReq() {
supportProvider.onAppInfoReq();
}
@Override
public void onAppStart(final UUID uuid, boolean start) {
if (start) {
supportProvider.onAppStart(uuid, start);
}
}
@Override
public void onAppDelete(final UUID uuid) {
supportProvider.onAppDelete(uuid);
}
}

View File

@ -20,6 +20,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.Uri;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -44,7 +45,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier;
@ -65,7 +65,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -79,14 +80,17 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetGpsParameterRequest;
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.SendExtendedAccountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherCurrentRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyRestHeartRateCapabilityRequest;
@ -178,6 +182,9 @@ public class HuaweiSupportProvider {
private final HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider();
protected ResponseManager responseManager = new ResponseManager(this);
protected HuaweiUploadManager huaweiUploadManager = new HuaweiUploadManager(this);
protected HuaweiWatchfaceManager huaweiWatchfaceManager = new HuaweiWatchfaceManager(this);
public HuaweiCoordinatorSupplier getCoordinator() {
return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator());
@ -186,6 +193,9 @@ public class HuaweiSupportProvider {
public HuaweiCoordinator getHuaweiCoordinator() {
return getCoordinator().getHuaweiCoordinator();
}
public HuaweiWatchfaceManager getHuaweiWatchfaceManager() {
return huaweiWatchfaceManager;
}
public HuaweiSupportProvider(HuaweiBRSupport support) {
this.brSupport = support;
@ -228,11 +238,6 @@ public class HuaweiSupportProvider {
}
public void setGps(boolean start) {
EventHandler handler;
if (isBLE())
handler = leSupport;
else
handler = brSupport;
if (start) {
if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false))
return;
@ -241,7 +246,7 @@ public class HuaweiSupportProvider {
gpsParameterRequest.setFinalizeReq(new RequestCallback() {
@Override
public void call() {
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
}
});
try {
@ -251,9 +256,9 @@ public class HuaweiSupportProvider {
LOG.error("Failed to get GPS parameters", e);
}
} else
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else
GBLocationManager.stop(getContext(), handler);
GBLocationService.stop(getContext(), getDevice());
}
public void setGpsParametersResponse(GpsAndTime.GpsParameters.Response response) {
@ -497,7 +502,6 @@ public class HuaweiSupportProvider {
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT, "p_on");
editor.apply();
setTrusleep();
}
onSetTime();
getBatteryLevel();
@ -720,6 +724,9 @@ public class HuaweiSupportProvider {
if (getHuaweiCoordinator().supportsActivityReminder()) {
setActivityReminder();
}
if (getHuaweiCoordinator().supportsTruSleep()) {
setTrusleep();
}
if (getHuaweiCoordinator().supportsPromptPushMessage() && getProtocolVersion() == 2) {
GetNotificationCapabilitiesRequest getNotificationCapabilitiesReq = new GetNotificationCapabilitiesRequest(this);
getNotificationCapabilitiesReq.doPerform();
@ -728,6 +735,11 @@ public class HuaweiSupportProvider {
GetNotificationConstraintsRequest getNotificationConstraintsReq = new GetNotificationConstraintsRequest(this);
getNotificationConstraintsReq.doPerform();
}
if (getHuaweiCoordinator().supportsWatchfaceParams()) {
GetWatchfaceParams getWatchfaceParams = new GetWatchfaceParams(this);
getWatchfaceParams.doPerform();
}
} catch (IOException e) {
GB.toast(getContext(), "Initialize dynamic services of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR,
e);
@ -1825,4 +1837,66 @@ public class HuaweiSupportProvider {
LOG.error("Failed to send GPS data", e);
}
}
public void onInstallApp(Uri uri) {
LOG.info("enter onAppInstall uri: "+uri);
HuaweiFwHelper huaweiFwHelper = new HuaweiFwHelper(uri, getContext());
huaweiUploadManager.setBytes(huaweiFwHelper.getBytes());
huaweiUploadManager.setFileType(huaweiFwHelper.getFileType());
if (huaweiFwHelper.isWatchface()) {
huaweiUploadManager.setFileName(huaweiWatchfaceManager.getRandomName());
} else {
huaweiUploadManager.setFileName(huaweiFwHelper.getFileName());
}
try {
SendFileUploadInfo sendFileUploadInfo = new SendFileUploadInfo(this, huaweiUploadManager);
sendFileUploadInfo.doPerform();
} catch (IOException e) {
GB.toast(context, "Failed to send file upload info", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to send file upload info", e);
}
}
public void onUploadProgress(int textRsrc, int progressPercent, boolean ongoing) {
try {
if (isBLE()) {
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = createLeTransactionBuilder("FetchRecordedData");
leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction(
context.getString(textRsrc),
ongoing,
progressPercent,
context
));
leBuilder.queue(leSupport.getQueue());
} else {
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = createBrTransactionBuilder("FetchRecordedData");
brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetProgressAction(
context.getString(textRsrc),
ongoing,
progressPercent,
context));
brBuilder.queue(brSupport.getQueue());
}
} catch (final Exception e) {
LOG.error("Failed to update progress notification", e);
}
}
public void onAppInfoReq() {
huaweiWatchfaceManager.requestWatchfaceList();
}
public void onAppStart(final UUID uuid, boolean start) {
if (start) {
huaweiWatchfaceManager.setWatchface(uuid);
}
}
public void onAppDelete(final UUID uuid) {
huaweiWatchfaceManager.deleteWatchface(uuid);
}
}

View File

@ -0,0 +1,149 @@
/* Copyright (C) 2024 Vitalii Tomin
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.net.Uri;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload.FileUploadParams;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
public class HuaweiUploadManager {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiUploadManager.class);
private final HuaweiSupportProvider support;
byte[] fileBin;
byte[] fileSHA256;
byte fileType = 1; // 1 - watchface, 2 - music, 3 - png for background , 7 - app
int fileSize = 0;
int currentUploadPosition = 0;
int uploadChunkSize =0;
String fileName = ""; //FIXME generate random name
//ack values set from 28 4 response
FileUploadParams fileUploadParams;
public HuaweiUploadManager(HuaweiSupportProvider support) {
this.support=support;
}
public void setBytes(byte[] uploadArray) {
this.fileSize = uploadArray.length;
this.fileBin = uploadArray;
try {
MessageDigest m = MessageDigest.getInstance("SHA256");
m.update(fileBin, 0, fileBin.length);
fileSHA256 = m.digest();
} catch (NoSuchAlgorithmException e) {
LOG.error("Digest alghoritm not found.", e);
return;
}
currentUploadPosition = 0;
uploadChunkSize = 0;
LOG.info("File ready for upload, SHA256: "+ GB.hexdump(fileSHA256) + " fileName: " + fileName + " filetype: ", fileType);
}
public int getFileSize() {
return fileSize;
}
public String getFileName() {
return this.fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte getFileType() {
return this.fileType;
}
public void setFileType(byte fileType) {
this.fileType = fileType;
}
public byte[] getFileSHA256() {
return fileSHA256;
}
public void setUploadChunkSize(int chunkSize) {
uploadChunkSize = chunkSize;
}
public void setCurrentUploadPosition (int pos) {
currentUploadPosition = pos;
}
public int getCurrentUploadPosition() {
return currentUploadPosition;
}
public byte[] getCurrentChunk() {
byte[] ret = new byte[uploadChunkSize];
System.arraycopy(fileBin, currentUploadPosition, ret, 0, uploadChunkSize);
return ret;
}
public void setFileUploadParams(FileUploadParams params) {
this.fileUploadParams = params;
}
public short getUnitSize() {
return fileUploadParams.unit_size;
}
public void setDeviceBusy() {
final GBDevice device = support.getDevice();
device.setBusyTask(support.getContext().getString(R.string.uploading_watchface));
device.sendDeviceUpdateIntent(support.getContext());
}
public void unsetDeviceBusy() {
final GBDevice device = support.getDevice();
if (device != null && device.isConnected()) {
if (device.isBusy()) {
device.unsetBusyTask();
device.sendDeviceUpdateIntent(support.getContext());
}
device.sendDeviceUpdateIntent(support.getContext());
}
}
}

View File

@ -0,0 +1,303 @@
/* Copyright (C) 2024 Vitalii Tomin
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface.WatchfaceDeviceParams;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfacesList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfacesNames;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
public class HuaweiWatchfaceManager
{
Logger LOG = LoggerFactory.getLogger(HuaweiCoordinator.class);
public static class Resolution {
Map<String, Object> map = new HashMap<>();
public Resolution() {
map.put("HWHD09", "466*466");
map.put("HWHD08", "320*320");
map.put("HWHD10", "360*320");
map.put("HWHD02", "454*454");
map.put("HWHD01", "390*390");
map.put("HWHD05", "460*188");
map.put("HWHD03", "240*120");
map.put("HWHD04", "160*80");
map.put("HWHD06", "456*280");
map.put("HWHD07", "368*194");
}
public boolean isValid(String themeVersion, String screenResolution) {
String screen = map.get(themeVersion).toString();
if (screenResolution.equals(screen)) {
return true;
} else {
return false;
}
}
public String screenByThemeVersion(String themeVersion) {
String screen = map.get(themeVersion).toString();
return screen;
}
}
public static class WatchfaceDescription {
public String title;
public String title_cn;
public String author;
public String designer;
public String screen;
public String version;
public String font;
public String font_cn;
public WatchfaceDescription(String xmlStr) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(
xmlStr)));
this.title = doc.getElementsByTagName("title").item(0).getTextContent();
this.title_cn = doc.getElementsByTagName("title-cn").item(0).getTextContent();
this.author = doc.getElementsByTagName("author").item(0).getTextContent();
this.designer = doc.getElementsByTagName("designer").item(0).getTextContent();
this.screen = doc.getElementsByTagName("screen").item(0).getTextContent();
this.version = doc.getElementsByTagName("version").item(0).getTextContent();
this.font = doc.getElementsByTagName("font").item(0).getTextContent();
this.font_cn = doc.getElementsByTagName("font-cn").item(0).getTextContent();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private List<Watchface.InstalledWatchfaceInfo> installedWatchfaceInfoList;
private HashMap<String, String> watchfacesNames;
private HuaweiSupportProvider support;
public HuaweiWatchfaceManager(HuaweiSupportProvider support) {
this.support = support;
}
public void setInstalledWatchfaceInfoList(List<Watchface.InstalledWatchfaceInfo> list) {
this.installedWatchfaceInfoList = list;
}
public List<Watchface.InstalledWatchfaceInfo> getInstalledWatchfaceInfoList()
{
return installedWatchfaceInfoList;
}
public void setWatchfacesNames(HashMap<String, String> map) {
this.watchfacesNames = map;
}
public String getRandomName() {
Random random = new Random();
String res="";
for (int i = 0; i < 9; i++) {
int ran = random.nextInt(9);
res += String.valueOf(ran);
}
res += "_1.0.0";
return res;
}
public static UUID toWatchfaceUUID(final String id) {
// Watchface IDs are numbers as strings - pad them to the right with F
// and encode as UUID
final String padded = String.format("%-32s", id).replace(' ', 'F');
return UUID.fromString(
padded.substring(0, 8) + "-" +
padded.substring(8, 12) + "-" +
padded.substring(12, 16) + "-" +
padded.substring(16, 20) + "-" +
padded.substring(20, 32)
);
}
public static String toWatchfaceId(final UUID uuid) {
return uuid.toString()
.replaceAll("-", "")
.replaceAll("f", "")
.replaceAll("F", "");
}
public void handleWatchfaceList() {
final List<GBDeviceApp> gbDeviceApps = new ArrayList<>();
for (final Watchface.InstalledWatchfaceInfo watchfaceInfo : installedWatchfaceInfoList) {
final UUID uuid = toWatchfaceUUID(watchfaceInfo.fileName);
GBDeviceApp gbDeviceApp = new GBDeviceApp(
uuid,
watchfacesNames.get(watchfaceInfo.fileName),
"",
"",
GBDeviceApp.Type.WATCHFACE
);
gbDeviceApps.add(gbDeviceApp);
}
final GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo();
appInfoCmd.apps = gbDeviceApps.toArray(new GBDeviceApp[0]);
support.evaluateGBDeviceEvent(appInfoCmd);
}
public void updateWatchfaceNames() {
Request.RequestCallback finalizeReq = new Request.RequestCallback() {
@Override
public void call() {
handleWatchfaceList();
}
@Override
public void handleException(Request.ResponseParseException e) {
LOG.error("Watchface update list exception", e);
}
};
try {
GetWatchfacesNames getWatchfacesNames = new GetWatchfacesNames(support, installedWatchfaceInfoList);
getWatchfacesNames.setFinalizeReq(finalizeReq);
getWatchfacesNames.doPerform();
} catch (IOException e) {
LOG.error("Could not get watchface names", e);
}
}
public void requestWatchfaceList() {
Request.RequestCallback finalizeReq = new Request.RequestCallback() {
@Override
public void call() {
updateWatchfaceNames();
}
@Override
public void handleException(Request.ResponseParseException e) {
LOG.error("Watchface update list exception", e);
}
};
try {
GetWatchfacesList getWatchfacesList = new GetWatchfacesList(support);
getWatchfacesList.setFinalizeReq(finalizeReq);
getWatchfacesList.doPerform();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
public void setWatchface(UUID uuid) {
Request.RequestCallback finalizeReq = new Request.RequestCallback() {
@Override
public void call() {
requestWatchfaceList();
}
@Override
public void handleException(Request.ResponseParseException e) {
LOG.error("Watchface update list exception", e);
}
};
try {
SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(support,
getFullFileName(uuid),
Watchface.WatchfaceOperation.operationActive);
sendWatchfaceOperation.setFinalizeReq(finalizeReq);
sendWatchfaceOperation.doPerform();
} catch (IOException e) {
LOG.error("Could not set watchface ", getFullFileName(uuid), e );
}
}
public void deleteWatchface(UUID uuid) {
Request.RequestCallback finalizeReq = new Request.RequestCallback() {
@Override
public void call() {
requestWatchfaceList();
}
@Override
public void handleException(Request.ResponseParseException e) {
LOG.error("Watchface update list exception", e);
}
};
try {
SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(support,
getFullFileName(uuid),
Watchface.WatchfaceOperation.operationDelete);
sendWatchfaceOperation.setFinalizeReq(finalizeReq);
sendWatchfaceOperation.doPerform();
} catch (IOException e) {
LOG.error("Could not delete watchface", getFullFileName(uuid), e);
}
}
private String getFullFileName(UUID uuid) {
String name = toWatchfaceId(uuid);
String version = "";
for (final Watchface.InstalledWatchfaceInfo watchfaceInfo : installedWatchfaceInfoList) {
if (watchfaceInfo.fileName.equals(name)) {
version = watchfaceInfo.version;
break;
}
}
String filename = name + "_" + version;
return filename;
}
}

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2024 Vitalii Tomin
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class GetWatchfaceParams extends Request{
private static final Logger LOG = LoggerFactory.getLogger(GetWatchfaceParams.class);
public GetWatchfaceParams(HuaweiSupportProvider support) {
super(support);
this.serviceId = Watchface.id;
this.commandId = Watchface.WatchfaceParams.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Watchface.WatchfaceParams.Request(paramsProvider).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Watchface.WatchfaceParams.Response))
throw new ResponseTypeMismatchException(receivedPacket, Watchface.WatchfaceParams.Response.class);
Watchface.WatchfaceParams.Response resp = (Watchface.WatchfaceParams.Response)(receivedPacket);
supportProvider.getHuaweiCoordinator().setWatchfaceDeviceParams(resp.params);
}
}

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2024 Vitalii Tomin
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class GetWatchfacesList extends Request{
private static final Logger LOG = LoggerFactory.getLogger(GetWatchfacesList.class);
public GetWatchfacesList(HuaweiSupportProvider support) {
super(support);
this.serviceId = Watchface.id;
this.commandId = Watchface.DeviceWatchInfo.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Watchface.DeviceWatchInfo.Request(paramsProvider).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Watchface.DeviceWatchInfo.Response))
throw new ResponseTypeMismatchException(receivedPacket, Watchface.DeviceWatchInfo.Response.class);
Watchface.DeviceWatchInfo.Response resp = (Watchface.DeviceWatchInfo.Response)(receivedPacket);
supportProvider.getHuaweiWatchfaceManager().setInstalledWatchfaceInfoList(resp.watchfaceInfoList);
}
}

View File

@ -0,0 +1,60 @@
/* Copyright (C) 2024 Vitalii Tomin
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class GetWatchfacesNames extends Request{
private static final Logger LOG = LoggerFactory.getLogger(GetWatchfacesNames.class);
List<Watchface.InstalledWatchfaceInfo> watchfaceInfoList;
public GetWatchfacesNames(HuaweiSupportProvider support, List<Watchface.InstalledWatchfaceInfo> list) {
super(support);
this.serviceId = Watchface.id;
this.commandId = Watchface.WatchfaceNameInfo.id;
this.watchfaceInfoList = list;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Watchface.WatchfaceNameInfo.Request(paramsProvider, this.watchfaceInfoList).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Watchface.WatchfaceNameInfo.Response))
throw new ResponseTypeMismatchException(receivedPacket, Watchface.WatchfaceNameInfo.Response.class);
Watchface.WatchfaceNameInfo.Response resp = (Watchface.WatchfaceNameInfo.Response)(receivedPacket);
supportProvider.getHuaweiWatchfaceManager().setWatchfacesNames(resp.watchFaceNames);
}
}

View File

@ -21,7 +21,9 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,8 +39,11 @@ public class SendAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendAccountToDevice.Request(paramsProvider).serialize();
return new AccountRelated.SendAccountToDevice.Request(paramsProvider, account).serialize();
} catch (CryptoException e) {
throw new RequestCreationException(e);
}

View File

@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,10 +39,14 @@ public class SendExtendedAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendExtendedAccountToDevice.Request(
paramsProvider,
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization())
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization(),
account)
.serialize();
} catch (CryptoException e) {
throw new Request.RequestCreationException(e);

View File

@ -0,0 +1,46 @@
/* Copyright (C) 2024 Vitalii Tomin
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.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendFileUploadAck extends Request {
byte noEncryption = 0;
byte fileType = 1;
public SendFileUploadAck(HuaweiSupportProvider support, byte noEncryption, byte fileType) {
super(support);
this.serviceId = FileUpload.id;
this.commandId = FileUpload.FileUploadConsultAck.id;
this.noEncryption = noEncryption;
this.fileType = fileType;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new FileUpload.FileUploadConsultAck.Request(this.paramsProvider, this.noEncryption, this.fileType).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,45 @@
/* Copyright (C) 2024 Vitalii Tomin
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.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
public class SendFileUploadChunk extends Request {
HuaweiUploadManager huaweiUploadManager;
public SendFileUploadChunk(HuaweiSupportProvider support,
HuaweiUploadManager watchfaceManager) {
super(support);
this.huaweiUploadManager = watchfaceManager;
this.serviceId = FileUpload.id;
this.commandId = FileUpload.FileNextChunkSend.id;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
return new FileUpload.FileNextChunkSend(this.paramsProvider).serializeFileChunk(
huaweiUploadManager.getCurrentChunk(),
huaweiUploadManager.getCurrentUploadPosition(),
huaweiUploadManager.getUnitSize()
);
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2024 Vitalii Tomin
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.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendFileUploadComplete extends Request {
byte fileType = 0;
public SendFileUploadComplete(HuaweiSupportProvider support, byte fileType) {
super(support);
this.serviceId = FileUpload.id;
this.commandId = FileUpload.FileUploadResult.id;
this.fileType = fileType;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new FileUpload.FileUploadResult.Request(this.paramsProvider, this.fileType).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2024 Vitalii Tomin
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.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
public class SendFileUploadHash extends Request{
HuaweiUploadManager huaweiUploadManager;
public SendFileUploadHash(HuaweiSupportProvider support,
HuaweiUploadManager huaweiUploadManager) {
super(support);
this.huaweiUploadManager = huaweiUploadManager;
this.serviceId = FileUpload.id;
this.commandId = FileUpload.FileHashSend.id;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new FileUpload.FileHashSend.Request(this.paramsProvider,
huaweiUploadManager.getFileSHA256(),
huaweiUploadManager.getFileType()
).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2024 Vitalii Tomin
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.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
public class SendFileUploadInfo extends Request{
HuaweiUploadManager huaweiUploadManager;
public SendFileUploadInfo(HuaweiSupportProvider support,
HuaweiUploadManager huaweiUploadManager) {
super(support);
this.huaweiUploadManager = huaweiUploadManager;
this.serviceId = FileUpload.id;
this.commandId = FileUpload.FileInfoSend.id;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new FileUpload.FileInfoSend.Request(this.paramsProvider,
huaweiUploadManager.getFileSize(),
huaweiUploadManager.getFileName(),
huaweiUploadManager.getFileType()
).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,46 @@
/* Copyright (C) 2024 Vitalii Tomin
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.Watchface;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendWatchfaceConfirm extends Request {
private String fileName;
public SendWatchfaceConfirm(HuaweiSupportProvider support, String filename) {
super(support);
this.serviceId = Watchface.id;
this.commandId = Watchface.WatchfaceConfirm.id;
this.fileName = filename;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Watchface.WatchfaceConfirm.Request(this.paramsProvider, this.fileName ).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,46 @@
/* Copyright (C) 2024 Vitalii Tomin
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.Watchface;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendWatchfaceOperation extends Request {
private String fileName;
private byte operation;
public SendWatchfaceOperation(HuaweiSupportProvider support, String filename, byte operation) {
super(support);
this.serviceId = Watchface.id;
this.commandId = Watchface.WatchfaceOperation.id;
this.fileName = filename;
this.operation = operation;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Watchface.WatchfaceOperation.Request(this.paramsProvider, this.fileName, this.operation ).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -106,6 +106,11 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
thisSync.add(calendarEvent);
int notifyMinutesBefore = 0;
if (!calendarEvent.getRemindersAbsoluteTs().isEmpty()) {
notifyMinutesBefore = (int) ((calendarEvent.getBeginSeconds() * 1000L - calendarEvent.getRemindersAbsoluteTs().get(0)) / (1000 * 60));
}
final XiaomiProto.CalendarEvent xiaomiCalendarEvent = XiaomiProto.CalendarEvent.newBuilder()
.setTitle(calendarEvent.getTitle())
.setDescription(StringUtils.ensureNotNull(calendarEvent.getDescription()))
@ -113,7 +118,7 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
.setStart(calendarEvent.getBeginSeconds())
.setEnd((int) (calendarEvent.getEnd() / 1000))
.setAllDay(calendarEvent.isAllDay())
.setNotifyMinutesBefore(0) // TODO fetch from event
.setNotifyMinutesBefore(notifyMinutesBefore)
.build();
calendarSync.addEvent(xiaomiCalendarEvent);

View File

@ -48,7 +48,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -664,7 +665,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
if (!gpsStarted) {
gpsStarted = true;
gpsFixAcquired = false;
GBLocationManager.start(getSupport().getContext(), getSupport());
GBLocationService.start(getSupport().getContext(), getSupport().getDevice(), GBLocationProviderType.GPS, 1000);
}
gpsTimeoutHandler.removeCallbacksAndMessages(null);
@ -673,7 +674,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
LOG.debug("Timed out waiting for workout");
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
}, 5000);
}
@ -696,7 +697,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
case WORKOUT_FINISHED:
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
if (startOnPhone) {
OpenTracksController.stopRecording(getSupport().getContext());
}

View File

@ -500,27 +500,6 @@ public class GB {
}
}
public static void createGpsNotification(Context context, int numDevices) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, numDevices))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
notify(NOTIFICATION_ID_GPS, nb.build(), context);
}
public static void removeGpsNotification(Context context) {
removeNotification(NOTIFICATION_ID_GPS, context);
}
private static Notification createInstallNotification(String text, boolean ongoing,
int percentage, Context context) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);

View File

@ -16,21 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.calendar;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class CalendarEvent {
private long begin;
private long end;
private long id;
private String title;
private String description;
private String location;
private String calName;
private String calAccountName;
private int color;
private boolean allDay;
private final long begin;
private final long end;
private final long id;
private final String title;
private final String description;
private final String location;
private final String calName;
private final String calAccountName;
private final String organizer;
private final int color;
private final boolean allDay;
private List<Long> remindersAbsoluteTs = new ArrayList<>();
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay) {
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay, String organizer) {
this.begin = begin;
this.end = end;
this.id = id;
@ -41,6 +45,15 @@ public class CalendarEvent {
this.calAccountName = calAccountName;
this.color = color;
this.allDay = allDay;
this.organizer = organizer;
}
public List<Long> getRemindersAbsoluteTs() {
return remindersAbsoluteTs;
}
public void setRemindersAbsoluteTs(List<Long> remindersAbsoluteTs) {
this.remindersAbsoluteTs = remindersAbsoluteTs;
}
public long getBegin() {
@ -76,6 +89,10 @@ public class CalendarEvent {
return title;
}
public String getOrganizer() {
return organizer;
}
public String getDescription() {
return description;
}
@ -117,7 +134,9 @@ public class CalendarEvent {
Objects.equals(this.getCalName(), e.getCalName()) &&
Objects.equals(this.getCalAccountName(), e.getCalAccountName()) &&
(this.getColor() == e.getColor()) &&
(this.isAllDay() == e.isAllDay());
(this.isAllDay() == e.isAllDay()) &&
Objects.equals(this.getOrganizer(), e.getOrganizer()) &&
Objects.equals(this.getRemindersAbsoluteTs(), e.getRemindersAbsoluteTs());
} else {
return false;
}
@ -135,6 +154,8 @@ public class CalendarEvent {
result = 31 * result + Objects.hash(calAccountName);
result = 31 * result + Integer.valueOf(color).hashCode();
result = 31 * result + Boolean.valueOf(allDay).hashCode();
result = 31 * result + Objects.hash(organizer);
result = 31 * result + Objects.hash(remindersAbsoluteTs);
return result;
}
}

View File

@ -36,7 +36,6 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -60,10 +59,12 @@ public class CalendarManager {
Instances.TITLE,
Instances.DESCRIPTION,
Instances.EVENT_LOCATION,
Instances.ORGANIZER,
Instances.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
Instances.CALENDAR_COLOR,
Instances.ALL_DAY
Instances.ALL_DAY,
Instances.EVENT_ID //needed for reminders
};
private static final int lookahead_days = 7;
@ -98,26 +99,54 @@ public class CalendarManager {
return calendarEventList;
}
while (evtCursor.moveToNext()) {
long start = evtCursor.getLong(1);
long end = evtCursor.getLong(2);
long start = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.BEGIN));
long end = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.END));
if (end == 0) {
LOG.info("no end time, will parse duration string");
Time time = new Time(); //FIXME: deprecated FTW
time.parse(evtCursor.getString(3));
time.parse(evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DURATION)));
end = start + time.toMillis(false);
}
CalendarEvent calEvent = new CalendarEvent(
start,
end,
evtCursor.getLong(0),
evtCursor.getString(4),
evtCursor.getString(5),
evtCursor.getString(6),
evtCursor.getString(7),
evtCursor.getString(8),
evtCursor.getInt(9),
!evtCursor.getString(10).equals("0")
evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances._ID)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.TITLE)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DESCRIPTION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.EVENT_LOCATION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_DISPLAY_NAME)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_NAME)),
evtCursor.getInt(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_COLOR)),
!evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ALL_DAY)).equals("0"),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ORGANIZER))
);
// Query reminders for this event
final Cursor reminderCursor = mContext.getContentResolver().query(
CalendarContract.Reminders.CONTENT_URI,
null,
CalendarContract.Reminders.EVENT_ID + " = ?",
new String[]{String.valueOf(evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.EVENT_ID)))},
null
);
if (reminderCursor != null && reminderCursor.getCount() > 0) {
final List<Long> reminders = new ArrayList<>();
while (reminderCursor.moveToNext()) {
int minutes = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.MINUTES));
int method = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.METHOD));
LOG.debug("Reminder Method: {}, Minutes: {}", method, minutes);
if (method == 1) //METHOD_ALERT
reminders.add(calEvent.getBegin() - minutes * 60 * 1000L);
}
reminderCursor.close();
calEvent.setRemindersAbsoluteTs(reminders);
}
if (!calendarIsBlacklisted(calEvent.getUniqueCalName())) {
calendarEventList.add(calEvent);
} else {

View File

@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.language.impl.PersianTransliter
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.PolishTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.RussianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.ScandinavianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.SerbianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.TurkishTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.UkranianTransliterator;
@ -85,6 +86,7 @@ public class LanguageUtils {
put("polish", new PolishTransliterator());
put("russian", new RussianTransliterator());
put("scandinavian", new ScandinavianTransliterator());
put("serbian", new SerbianTransliterator());
put("turkish", new TurkishTransliterator());
put("ukranian", new UkranianTransliterator());
put("armenian", new ArmenianTransliterator());

View File

@ -18,14 +18,19 @@ package nodomain.freeyourgadget.gadgetbridge.util.language;
import org.apache.commons.lang3.text.WordUtils;
import java.text.Normalizer;
import java.util.Map;
public class SimpleTransliterator implements Transliterator {
private final Map<Character, String> transliterateMap;
private final boolean convertToLowercase;
public SimpleTransliterator(final Map<Character, String> transliterateMap, final boolean convertToLowercase) {
this.transliterateMap = transliterateMap;
this.convertToLowercase = convertToLowercase;
}
public SimpleTransliterator(final Map<Character, String> transliterateMap) {
this.transliterateMap = transliterateMap;
this(transliterateMap, true);
}
@Override
@ -46,14 +51,14 @@ public class SimpleTransliterator implements Transliterator {
return message;
}
private String transliterate(char c) {
final char lowerChar = Character.toLowerCase(c);
private String transliterate(final char c) {
final char sourceChar = convertToLowercase ? Character.toLowerCase(c) : c;
if (transliterateMap.containsKey(lowerChar)) {
final String replace = transliterateMap.get(lowerChar);
if (transliterateMap.containsKey(sourceChar)) {
final String replace = transliterateMap.get(sourceChar);
if (lowerChar != c) {
return WordUtils.capitalize(replace);
if (sourceChar != c) {
return convertToLowercase ? WordUtils.capitalize(replace) : replace;
}
return replace;

View File

@ -0,0 +1,68 @@
/* Copyright (C) 2024 José Rebelo
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.util.language.impl;
import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.util.language.SimpleTransliterator;
public class SerbianTransliterator extends SimpleTransliterator {
public SerbianTransliterator() {
super(new HashMap<Character, String>() {{
// As per https://en.wikipedia.org/wiki/Serbian_Cyrillic_alphabet#Modern_alphabet
put('А', "A"); put('а', "a");
put('Б', "B"); put('б', "b");
put('В', "V"); put('в', "v");
put('Г', "G"); put('г', "g");
put('Д', "D"); put('д', "d");
put('Ђ', "Dj"); put('ђ', "dj"); // from Đ / đ - from suggestion in #3727
put('Е', "E"); put('е', "e");
put('Ж', "Z"); put('ж', "z"); // from Ž / ž
put('З', "Z"); put('з', "z");
put('И', "I"); put('и', "i");
put('Ј', "J"); put('ј', "j");
put('К', "K"); put('к', "k");
put('Л', "L"); put('л', "l");
put('Љ', "Lj"); put('љ', "lj");
put('М', "M"); put('м', "m");
put('Н', "N"); put('н', "n");
put('Њ', "Nj"); put('њ', "nj");
put('О', "O"); put('о', "o");
put('П', "P"); put('п', "p");
put('Р', "R"); put('р', "r");
put('С', "S"); put('с', "s");
put('Т', "T"); put('т', "t");
put('Ћ', "C"); put('ћ', "c"); // from Ć / ć
put('У', "U"); put('у', "u");
put('Ф', "F"); put('ф', "f");
put('Х', "H"); put('х', "h");
put('Ц', "C"); put('ц', "c");
put('Ч', "C"); put('ч', "c"); // from Č / č
put('Џ', "Dz"); put('џ', "dz"); // from /
put('Ш', "S"); put('ш', "s"); // From Š / š
// Not in the table, pulled from Croatian
put('Ć', "C"); put('ć', "c");
put('Đ', "Dj"); put('đ', "dj");
put('Š', "S"); put('š', "s");
put('Ž', "z"); put('ž', "z");
// Suggested in #3727
put('Č', "C"); put('č', "c");
}}, false);
}
}

View File

@ -3492,6 +3492,7 @@
<item>@string/polish</item>
<item>@string/russian</item>
<item>@string/scandinavian</item>
<item>@string/serbian</item>
<item>@string/turkish</item>
<item>@string/ukranian</item>
<item>@string/hungarian</item>
@ -3519,6 +3520,7 @@
<item>polish</item>
<item>russian</item>
<item>scandinavian</item>
<item>serbian</item>
<item>turkish</item>
<item>ukranian</item>
<item>hungarian</item>

View File

@ -1060,6 +1060,7 @@
<string name="lithuanian">Lithuanian</string>
<string name="persian">Persian</string>
<string name="scandinavian">Scandinavian</string>
<string name="serbian">Serbian</string>
<string name="ukranian">Ukranian</string>
<string name="armenian">Armenian</string>
<string name="italian">Italian</string>
@ -2812,4 +2813,7 @@
<string name="pref_sleepasandroid_feat_heartrate">Heart rate</string>
<string name="pref_sleepasandroid_feat_oximetry">Oximetry</string>
<string name="pref_sleepasandroid_feat_spo2">SPO2</string>
<string name="pref_title_huawei_account">Huawei Account</string>
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
<string name="watchface_resolution_doesnt_match">Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:icon="@drawable/ic_vpn_key"
android:key="huawei_account"
android:maxLength="17"
android:summary="@string/pref_summary_huawei_account"
android:title="@string/pref_title_huawei_account" />
</androidx.preference.PreferenceScreen>

View File

@ -8,7 +8,6 @@
android:summary="@string/huawei_trusleep_summary_light">
<SwitchPreferenceCompat
android:enabled="false"
android:icon="@drawable/ic_access_time"
android:defaultValue="false"
android:key="trusleep"

View File

@ -25,22 +25,25 @@ public class CalendarEventTest extends TestBase {
@Test
public void testHashCode() {
CalendarEvent c1 =
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c2 =
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c3 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c4 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, "some");
assertEquals(c1.hashCode(), c1.hashCode());
assertNotEquals(c1.hashCode(), c2.hashCode());
assertNotEquals(c2.hashCode(), c3.hashCode());
assertNotEquals(c3.hashCode(), c4.hashCode());
}
@Test
public void testSync() {
List<CalendarEvent> eventList = new ArrayList<>();
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
GBDevice dummyGBDevice = createDummyGDevice("00:00:01:00:03");
dummyGBDevice.setState(GBDevice.State.INITIALIZED);
@ -49,7 +52,7 @@ public class CalendarEventTest extends TestBase {
testCR.syncCalendar(eventList);
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
testCR.syncCalendar(eventList);
CalendarSyncStateDao calendarSyncStateDao = daoSession.getCalendarSyncStateDao();

View File

@ -5,6 +5,8 @@ import android.content.SharedPreferences;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -44,6 +46,34 @@ public class LanguageUtilsTest extends TestBase {
assertEquals("Transliteration failed", result, output);
}
@Test
public void testStringTransliterateSerbian() throws Exception {
final Transliterator transliterator = LanguageUtils.getTransliterator("serbian");
final Map<String, String> tests = new LinkedHashMap<String, String>() {{
put("Тхе qицк брон фоx јумпед овер тхе лаз* дог", "The qick bron fox jumped over the laz* dog");
put("Српска ћирилица", "Srpska cirilica");
put("Novak Đoković", "Novak Dokovic");
put("Џ, Њ and Љ", "Dz, Nj and Lj");
put("Љуљачка", "Ljuljacka");
put("Наковањ", "Nakovanj");
put("Качкаваљ", "Kackavalj");
put("Чачак", "Cacak");
put("Ч, ч", "C, c");
put("Ћ, ћ", "C, c");
put("Ж, ж", "Z, z");
put("Ш, ш", "S, s");
put("Ђ, ђ", "D, d");
put("Џ, џ", "Dz, dz");
put("Њ, њ", "Nj, nj");
put("Љ, љ", "Lj, lj");
}};
for (final Map.Entry<String, String> e : tests.entrySet()) {
assertEquals("Transliteration failed for " + e.getKey(), e.getValue(), transliterator.transliterate(e.getKey()));
}
}
@Test
public void testStringTransliterateHebrew() throws Exception {
final Transliterator transliterator = LanguageUtils.getTransliterator("hebrew");