Compare commits

...

9 Commits

Author SHA1 Message Date
aGoodUsername 08a56d3b9b bangle.js: normalized activity intensity 2024-04-26 07:16:56 +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
40 changed files with 611 additions and 431 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

@ -82,7 +82,7 @@ public class BangleJSSampleProvider extends AbstractSampleProvider<BangleJSActiv
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / 256.0f;
return rawIntensity / 2048.0f;
}
@Override

View File

@ -30,6 +30,7 @@ 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.devicesettings.DeviceSpecificSettingsCustomizer;
@ -83,6 +84,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;

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

@ -30,6 +30,7 @@ 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.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
@ -82,7 +83,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;

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

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

@ -49,8 +49,6 @@ 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.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.SendMenstrualModifyTimeRequest;

View File

@ -44,7 +44,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 +64,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;
@ -228,11 +228,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 +236,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 +246,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) {

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

@ -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,6 @@
<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>
</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

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