/* Copyright (C) 2019-2021 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno, Arjan Schrijver This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.FitnessConfigItem; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.InactivityWarningItem; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.UnitsConfigItem; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest.VibrationStrengthConfigItem; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_PHONE_REQUEST; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_WATCH_REQUEST; import static nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil.convertDrawableToBitmap; import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID; import static nodomain.freeyourgadget.gadgetbridge.util.StringUtils.shortenPackageName; import android.app.Service; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.widget.Toast; import androidx.core.app.NotificationCompat; import androidx.core.content.res.ResourcesCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import net.e175.klaus.solarpositioning.DeltaT; import net.e175.klaus.solarpositioning.SPA; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CommuteActionsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample; import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityEntry; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityFileParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileDeleteRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRawRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRawRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.DismissTextNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayCallNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayTextNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.alexa.AlexaMessageSetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.application.ApplicationInformation; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.application.ApplicationsListRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.async.ConfirmAppStatusRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.CheckDeviceNeedsConfirmationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.CheckDevicePairingRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.ConfirmOnDeviceRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.PerformDevicePairingRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfiguration; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.commute.CommuteConfigPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationGetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedInterface; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImage; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImageFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImagesSetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.json.JsonPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.menu.SetCommuteMenuMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicInfoSetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImage; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply.QuickReplyConfigurationPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply.QuickReplyConfirmationPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.theme.SelectedThemePutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomBackgroundWidgetElement; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomTextWidgetElement; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidget; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidgetElement; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.Widget; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.WidgetsPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.workout.WorkoutRequestHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FactoryResetRequest; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; import nodomain.freeyourgadget.gadgetbridge.util.Version; public class FossilHRWatchAdapter extends FossilWatchAdapter { public static final int MESSAGE_WHAT_VOICE_DATA_RECEIVED = 0; private byte[] phoneRandomNumber; private byte[] watchRandomNumber; private static ArrayList widgets = new ArrayList<>(); private NotificationHRConfiguration[] notificationConfigurations; private CallSpec currentCallSpec = null; private MusicSpec currentSpec = null; private byte jsonIndex = 0; private AssetImage backGroundImage = null; public FossilHRWatchAdapter(QHybridSupport deviceSupport) { super(deviceSupport); } private boolean saveRawActivityFiles = false; private boolean notifiedAboutMissingNavigationApp = false; HashMap appIconCache = new HashMap<>(); String lastPostedApp = null; List installedApplications = new ArrayList(); Messenger voiceMessenger = null; private Version cleanFirmwareVersion = null; ServiceConnection voiceServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { GB.log("attached to voice service", GB.INFO, null); voiceMessenger = new Messenger(service); } @Override public void onServiceDisconnected(ComponentName name) { GB.log("detached from voice service", GB.INFO, null); voiceMessenger = null; } }; enum CONNECTION_MODE { NOT_INITIALIZED, AUTHENTICATED, NOT_AUTHENTICATED } CONNECTION_MODE connectionMode = CONNECTION_MODE.NOT_INITIALIZED; @Override public void initialize() { timeoutThread.start(); saveRawActivityFiles = getDeviceSpecificPreferences().getBoolean("save_raw_activity_files", false); queueWrite(new RequestMtuRequest(512)); listApplications(); getDeviceInfos(); } @Override protected void initializeWithSupportedFileVersions() { if (getDeviceSupport().getDevice().getFirmwareVersion().contains("prod")) { GB.toast("Dummy FW, skipping initialization", Toast.LENGTH_LONG, GB.INFO); queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED), false); return; } queueWrite(new SetDeviceStateRequest(GBDevice.State.AUTHENTICATING)); negotiateSymmetricKey(); } public void listApplications() { queueWrite(new ApplicationsListRequest(this){ @Override public void handleApplicationsList(List installedApplications) { ((FossilHRWatchAdapter) getAdapter()).setInstalledApplications(installedApplications); GBDevice device = getAdapter().getDeviceSupport().getDevice(); JSONArray array = new JSONArray(); for(ApplicationInformation info : installedApplications){ array.put(info.getAppName()); } device.addDeviceInfo(new GenericItem("INSTALLED_APPS", array.toString())); device.sendDeviceUpdateIntent(getAdapter().getContext()); } }); } private void initializeAfterAuthentication(boolean authenticated) { queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZING)); if (!authenticated) { GB.toast(getContext().getString(R.string.fossil_hr_auth_failed), Toast.LENGTH_LONG, GB.ERROR); initializeAfterWatchConfirmation(false); return; } boolean versionSupportsConfirmation = getCleanFWVersion().greaterOrEqualThan(new Version("2.22")); if(!versionSupportsConfirmation){ initializeAfterWatchConfirmation(true); return; } boolean shouldAuthenticateOnWatch = getDeviceSpecificPreferences().getBoolean("enable_on_device_confirmation", true); if (!shouldAuthenticateOnWatch) { GB.toast(getContext().getString(R.string.fossil_hr_confirmation_skipped), Toast.LENGTH_SHORT, GB.INFO); initializeAfterWatchConfirmation(false); return; } confirmOnWatch(); } TimerTask confirmTimeoutRunnable = new TimerTask() { @Override public void run() { if (!(fossilRequest instanceof ConfirmOnDeviceRequest)) { return; } GB.toast(getContext().getString(R.string.fossil_hr_confirmation_timeout), Toast.LENGTH_SHORT, GB.INFO); ((ConfirmOnDeviceRequest) fossilRequest).onResult(false); } }; private void confirmOnWatch() { queueWrite(new CheckDeviceNeedsConfirmationRequest() { @Override public void onResult(boolean needsConfirmation) { GB.log("needs confirmation: " + needsConfirmation, GB.INFO, null); if (needsConfirmation) { final Timer timer = new Timer(); GB.toast(getContext().getString(R.string.fossil_hr_confirm_connection), Toast.LENGTH_SHORT, GB.INFO); queueWrite(new ConfirmOnDeviceRequest() { @Override public void onResult(boolean confirmationSuccess) { isFinished = true; timer.cancel(); if (confirmationSuccess) { pairToWatch(); } else { GB.toast(getContext().getString(R.string.fossil_hr_connection_not_confirmed), Toast.LENGTH_LONG, GB.ERROR); } initializeAfterWatchConfirmation(confirmationSuccess); } }, true); timer.schedule(confirmTimeoutRunnable, 30000); } else { initializeAfterWatchConfirmation(true); } } }); } private void pairToWatch() { queueWrite(new CheckDevicePairingRequest() { @Override public void onResult(boolean pairingStatus) { GB.log("watch pairing status: " + pairingStatus, GB.INFO, null); if (!pairingStatus) { queueWrite(new PerformDevicePairingRequest() { @Override public void onResult(boolean pairingSuccess) { isFinished = true; GB.log("watch pairing result: " + pairingSuccess, GB.INFO, null); if (pairingSuccess) { GB.toast(getContext().getString(R.string.fossil_hr_pairing_successful), Toast.LENGTH_LONG, GB.ERROR); } else { GB.toast(getContext().getString(R.string.fossil_hr_pairing_failed), Toast.LENGTH_LONG, GB.ERROR); } } }, true); } } }); } private void respondToAlexa(String message, boolean isResponse){ queueWrite(new AlexaMessageSetRequest(message, isResponse, this)); } private void attachToVoiceService(){ String servicePackage = getDeviceSpecificPreferences().getString("voice_service_package", ""); String servicePath = getDeviceSpecificPreferences().getString("voice_service_class", ""); if(servicePackage.isEmpty()){ GB.toast("voice service package is not configured", Toast.LENGTH_LONG, GB.ERROR); respondToAlexa("voice service package not configured on phone", true); return; } if(servicePath.isEmpty()){ respondToAlexa("voice service class not configured on phone", true); GB.toast("voice service class is not configured", Toast.LENGTH_LONG, GB.ERROR); return; } ComponentName component = new ComponentName(servicePackage, servicePath); // extract to somewhere Intent voiceIntent = new Intent("nodomain.freeyourgadget.gadgetbridge.VOICE_COMMAND"); voiceIntent.setComponent(component); int flags = 0; flags |= Service.BIND_AUTO_CREATE; GB.log("binding to voice service...", GB.INFO, null); getContext().bindService( voiceIntent, voiceServiceConnection, flags ); PackageManager pm = getContext().getPackageManager(); boolean serviceEnabled = true; try { int enabledState = pm.getComponentEnabledSetting(component); if(enabledState != PackageManager.COMPONENT_ENABLED_STATE_ENABLED && enabledState != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT){ respondToAlexa("voice service is disabled on phone", true); GB.toast("voice service is disabled", Toast.LENGTH_LONG, GB.ERROR); serviceEnabled = false; } }catch (IllegalArgumentException e){ serviceEnabled = false; respondToAlexa("voice service not found on phone", true); GB.toast("voice service not found", Toast.LENGTH_LONG, GB.ERROR); } if(!serviceEnabled){ detachFromVoiceService(); } } private void detachFromVoiceService(){ getContext().unbindService(voiceServiceConnection); } private void handleVoiceStatus(byte status){ if(status == 0x00){ attachToVoiceService(); }else if(status == 0x01){ detachFromVoiceService(); } } private void handleVoiceStatusCharacteristic(BluetoothGattCharacteristic characteristic){ byte[] value = characteristic.getValue(); handleVoiceStatus(value[0]); } private void handleVoiceDataCharacteristic(BluetoothGattCharacteristic characteristic){ if(voiceMessenger == null){ return; } Message message = Message.obtain( null, MESSAGE_WHAT_VOICE_DATA_RECEIVED ); Bundle dataBundle = new Bundle(1); dataBundle.putByteArray("VOICE_DATA", characteristic.getValue()); dataBundle.putString("VOICE_ENCODING", "OPUS"); message.setData(dataBundle); try { voiceMessenger.send(message); } catch (RemoteException e) { GB.log("error sending voice data to service", GB.ERROR, e); GB.toast("error sending voice data to service", Toast.LENGTH_LONG, GB.ERROR); voiceMessenger = null; detachFromVoiceService(); respondToAlexa("error sending voice data to service", true); } } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { switch (characteristic.getUuid().toString()){ case "010541ae-efe8-11c0-91c0-105d1a1155f0": handleVoiceStatusCharacteristic(characteristic); return true; case "842d2791-0d20-4ce4-1ada-105d1a1155f0": handleVoiceDataCharacteristic(characteristic); return true; } return super.onCharacteristicChanged(gatt, characteristic); } private void initializeAfterWatchConfirmation(boolean authenticated) { setNotificationConfigurations(); setQuickRepliesConfiguration(); if (authenticated) { // setVibrationStrengthFromConfig(); setUnitsConfig(); syncSettings(); setTime(); } overwriteButtons(null); loadBackground(); loadWidgets(); // renderWidgets(); // dunno if there is any point in doing this at start since when no watch is connected the QHybridSupport will not receive any intents anyway updateBuiltinAppsInCache(); queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED)); } private void handleAuthenticationResult(boolean success) { if (this.connectionMode != CONNECTION_MODE.NOT_INITIALIZED) return; this.connectionMode = success ? CONNECTION_MODE.AUTHENTICATED : CONNECTION_MODE.NOT_AUTHENTICATED; this.initializeAfterAuthentication(success); } @Override public void uninstallApp(String appName) { for (ApplicationInformation appInfo : this.installedApplications) { if (appInfo.getAppName().equals(appName)) { byte handle = appInfo.getFileHandle(); short fullFileHandle = (short) ((FileHandle.APP_CODE.getMajorHandle()) << 8 | handle); queueWrite(new FileDeleteRequest(fullFileHandle)); listApplications(); break; } } } public void activateWatchface(String appName) { queueWrite(new SelectedThemePutRequest(this, appName)); } public void startAppOnWatch(String appName) { try { JSONObject jsonObject = new JSONObject() .put("push", new JSONObject() .put("set", new JSONObject() .put("customWatchFace._.config.start_app", appName) ) ); queueWrite(new JsonPutRequest(jsonObject, this)); } catch (JSONException e) { LOG.warn("Couldn't start app on watch: " + e.getMessage()); } } public void downloadAppToCache(UUID uuid) { LOG.info("Going to download app with UUID " + uuid.toString()); for (ApplicationInformation appInfo : installedApplications) { if (UUID.nameUUIDFromBytes(appInfo.getAppName().getBytes(StandardCharsets.UTF_8)).equals(uuid)) { LOG.info("Going to download app with name " + appInfo.getAppName() + " and handle " + appInfo.getFileHandle()); downloadFile(FileHandle.APP_CODE.getMajorHandle(), appInfo.getFileHandle(), appInfo.getAppName(), false, true); } } } private void setVibrationStrengthFromConfig() { Prefs prefs = new Prefs(getDeviceSpecificPreferences()); int vibrationStrengh = prefs.getInt(DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE, 2); if (vibrationStrengh > 0) { vibrationStrengh = (vibrationStrengh + 1) * 25; // Seems 0,50,75,100 are working... } setVibrationStrength((short) (vibrationStrengh)); } private void setUnitsConfig() { Prefs prefs = GBApplication.getPrefs(); String unit = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric)); int value = 8; // dont know what this bit means but it was set for me before tampering if (!unit.equals("metric")) { value |= (4 | 1); // temperature and distance } queueWrite( (FileEncryptedInterface) new ConfigurationPutRequest(new UnitsConfigItem(value), this) ); } @Override public void setVibrationStrength(short strength) { if (connectionMode == CONNECTION_MODE.NOT_AUTHENTICATED) { GB.toast(getContext().getString(R.string.fossil_hr_unavailable_unauthed), Toast.LENGTH_LONG, GB.ERROR); return; } queueWrite( (FileEncryptedInterface) new ConfigurationPutRequest(new VibrationStrengthConfigItem((byte) strength), this) ); } private void setNotificationConfigurations() { // Set default icons ArrayList images = new ArrayList<>(); images.add(new NotificationImage("icIncomingCall.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_phone_outline)), 24, 24)); images.add(new NotificationImage("icMissedCall.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_phone_missed_outline)), 24, 24)); images.add(new NotificationImage("icMessage.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_message_outline)), 24, 24)); images.add(new NotificationImage("general_white.bin", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_alert_circle_outline)), 24, 24)); // Set default notification filters ArrayList notificationFilters = new ArrayList<>(); notificationFilters.add(new NotificationHRConfiguration("generic", "general_white.bin")); notificationFilters.add(new NotificationHRConfiguration("call", new byte[]{(byte) 0x80, (byte) 0x00, (byte) 0x59, (byte) 0xB7}, "icIncomingCall.icon")); // Add icons and notification filters from cached past notifications Set> entrySet = this.appIconCache.entrySet(); for (Map.Entry entry : entrySet) { String iconName = shortenPackageName(entry.getKey()) + ".icon"; images.add(new NotificationImage(iconName, entry.getValue())); notificationFilters.add(new NotificationHRConfiguration(entry.getKey(), iconName)); } // Send notification icons try { queueWrite(new NotificationImagePutRequest(images.toArray(new NotificationImage[images.size()]), this)); } catch (IOException e) { LOG.error("Error while sending notification icons", e); } // Send notification filters configuration this.notificationConfigurations = notificationFilters.toArray(new NotificationHRConfiguration[notificationFilters.size()]); queueWrite(new NotificationFilterPutHRRequest(this.notificationConfigurations, this)); } private String[] getQuickReplies() { ArrayList configuredReplies = new ArrayList<>(); Prefs prefs = new Prefs(getDeviceSpecificPreferences()); for (int i = 1; i <= 16; i++) { String quickReply = prefs.getString("canned_message_dismisscall_" + i, null); if (quickReply != null) { configuredReplies.add(quickReply); } } return configuredReplies.toArray(new String[0]); } public void setQuickRepliesConfiguration() { String[] quickReplies = getQuickReplies(); if (quickReplies.length > 0) { NotificationImage quickReplyIcon = new NotificationImage("icMessage.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_message_outline)), 24, 24); queueWrite(new NotificationImagePutRequest(quickReplyIcon, this)); queueWrite(new QuickReplyConfigurationPutRequest(quickReplies, this)); } } private File getBackgroundFile() { return new File(getContext().getExternalFilesDir(null), "hr_background.bin"); } private void loadBackground() { this.backGroundImage = null; try { FileInputStream fis = new FileInputStream(getBackgroundFile()); int count = fis.available(); if (count != 14400) { throw new RuntimeException("wrong background file length"); } byte[] file = new byte[14400]; fis.read(file); fis.close(); this.backGroundImage = AssetImageFactory.createAssetImage(file, 0, 0, 0); } catch (FileNotFoundException e) { SharedPreferences preferences = getDeviceSpecificPreferences(); if (preferences.getBoolean("force_white_color_scheme", false)) { Bitmap whiteBitmap = Bitmap.createBitmap(239, 239, Bitmap.Config.ARGB_8888); new Canvas(whiteBitmap).drawColor(Color.WHITE); try { this.backGroundImage = AssetImageFactory.createAssetImage(whiteBitmap, true, 0, 1, 0); } catch (IOException e2) { LOG.error("Backgroundimage error", e2); } } } catch (IOException | RuntimeException e) { GB.log("error opening background file", GB.ERROR, e); GB.toast("error opening background file", Toast.LENGTH_LONG, GB.ERROR); } } @Override public void setBackgroundImage(byte[] pixels) { if (pixels == null) { getBackgroundFile().delete(); loadBackground(); // recreates the white background in force-white mode, else backgroundImage=null } else { this.backGroundImage = AssetImageFactory.createAssetImage(pixels, 0, 0, 0); try { FileOutputStream fos = new FileOutputStream(getBackgroundFile(), false); fos.write(pixels); } catch (IOException e) { GB.log("error saving background", GB.ERROR, e); GB.toast("error persistent saving background", Toast.LENGTH_LONG, GB.ERROR); } } renderWidgets(); } private void loadWidgets() { Version firmwareVersion = getCleanFWVersion(); if (firmwareVersion != null && firmwareVersion.greaterOrEqualThan(new Version("2.20"))) { return; // this does not work on newer firmware versions } Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDeviceSupport().getDevice().getAddress())); boolean forceWhiteBackground = prefs.getBoolean("force_white_color_scheme", false); String fontColor = forceWhiteBackground ? "black" : "default"; Widget[] oldWidgets = widgets.toArray(new Widget[0]); widgets.clear(); String widgetJson = GBApplication.getPrefs().getPreferences().getString("FOSSIL_HR_WIDGETS", "{}"); String customWidgetJson = GBApplication.getPrefs().getString("QHYBRID_CUSTOM_WIDGETS", "[]"); try { JSONObject widgetConfig = new JSONObject(widgetJson); JSONArray customWidgets = new JSONArray(customWidgetJson); Iterator keyIterator = widgetConfig.keys(); HashMap positionMap = new HashMap<>(4); positionMap.put("top", 0); positionMap.put("right", 90); positionMap.put("bottom", 180); positionMap.put("left", 270); while (keyIterator.hasNext()) { String position = keyIterator.next(); String identifier = widgetConfig.getString(position); Widget.WidgetType type = Widget.WidgetType.fromJsonIdentifier(identifier); Widget widget = null; if (type != null) { widget = new Widget(type, positionMap.get(position), 63, fontColor); } else { identifier = identifier.substring(7); for (int i = 0; i < customWidgets.length(); i++) { JSONObject customWidget = customWidgets.getJSONObject(i); if (customWidget.getString("name").equals(identifier)) { boolean drawCircle = false; if (customWidget.has("drawCircle")) drawCircle = customWidget.getBoolean("drawCircle"); CustomWidget newWidget = new CustomWidget( customWidget.getString("name"), positionMap.get(position), 63, fontColor ); JSONArray elements = customWidget.getJSONArray("elements"); for (int i2 = 0; i2 < elements.length(); i2++) { JSONObject element = elements.getJSONObject(i2); if (element.getString("type").equals("text")) { newWidget.addElement(new CustomTextWidgetElement( element.getString("id"), element.getString("value"), element.getInt("x"), element.getInt("y") )); } else if (element.getString("type").equals("background")) { newWidget.addElement(new CustomBackgroundWidgetElement( element.getString("id"), element.getString("value") )); } } widget = newWidget; } } } if (widget == null) continue; widgets.add(widget); } } catch (JSONException e) { LOG.error("Error while updating widgets", e); } for (Widget oldWidget : oldWidgets) { if (!(oldWidget instanceof CustomWidget)) continue; CustomWidget customOldWidget = (CustomWidget) oldWidget; for (CustomWidgetElement oldElement : customOldWidget.getElements()) { for (Widget newWidget : widgets) { if (newWidget instanceof CustomWidget) { ((CustomWidget) newWidget).updateElementValue(oldElement.getId(), oldElement.getValue()); } } } } uploadWidgets(); } public void setInstalledApplications(List installedApplications) { this.installedApplications = installedApplications; GBDeviceEventAppInfo appInfoEvent = new GBDeviceEventAppInfo(); appInfoEvent.apps = new GBDeviceApp[installedApplications.size()]; for (int i = 0; i < installedApplications.size(); i++) { String appName = installedApplications.get(i).getAppName(); String appVersion = installedApplications.get(i).getAppVersion(); UUID appUUID = UUID.nameUUIDFromBytes(appName.getBytes(StandardCharsets.UTF_8)); GBDeviceApp.Type appType; if (installedApplications.get(i).getAppName().endsWith("App")) { appType = GBDeviceApp.Type.APP_GENERIC; } else { appType = GBDeviceApp.Type.WATCHFACE; } appInfoEvent.apps[i] = new GBDeviceApp(appUUID, appName, "(unknown)", appVersion, appType); } getDeviceSupport().evaluateGBDeviceEvent(appInfoEvent); } private void uploadWidgets() { ArrayList systemWidgets = new ArrayList<>(widgets.size()); for (Widget widget : widgets) { if (!(widget instanceof CustomWidget) && !widget.getWidgetType().isCustom()) systemWidgets.add(widget); } queueWrite(new WidgetsPutRequest(systemWidgets.toArray(new Widget[0]), this)); } private void renderWidgets() { Version firmwareVersion = getCleanFWVersion(); if (firmwareVersion != null && firmwareVersion.greaterOrEqualThan(new Version("2.20"))) { return; // this does not work on newer firmware versions } Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDeviceSupport().getDevice().getAddress())); boolean forceWhiteBackground = prefs.getBoolean("force_white_color_scheme", false); boolean drawCircles = prefs.getBoolean("widget_draw_circles", false); Bitmap circleBitmap = null; if (drawCircles) { circleBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888); Canvas circleCanvas = new Canvas(circleBitmap); Paint circlePaint = new Paint(); circlePaint.setAntiAlias(true); circlePaint.setColor(forceWhiteBackground ? Color.WHITE : Color.BLACK); circlePaint.setStyle(Paint.Style.FILL); circlePaint.setStrokeWidth(3); circleCanvas.drawCircle(38, 38, 35, circlePaint); circlePaint.setColor(forceWhiteBackground ? Color.BLACK : Color.WHITE); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setStrokeWidth(3); circleCanvas.drawCircle(38, 38, 35, circlePaint); } try { ArrayList widgetImages = new ArrayList<>(); if (this.backGroundImage != null) { widgetImages.add(this.backGroundImage); } for (int i = 0; i < widgets.size(); i++) { Widget w = widgets.get(i); if (!(w instanceof CustomWidget)) { if (w.getWidgetType() == Widget.WidgetType.LAST_NOTIFICATION) { Bitmap widgetBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888); Canvas widgetCanvas = new Canvas(widgetBitmap); if (drawCircles) { widgetCanvas.drawBitmap(circleBitmap, 0, 0, null); } Paint p = new Paint(); p.setStyle(Paint.Style.FILL); p.setTextSize(10); p.setColor(Color.WHITE); if (this.lastPostedApp != null) { Bitmap icon = Bitmap.createScaledBitmap(appIconCache.get(this.lastPostedApp), 40, 40, true); if (icon != null) { widgetCanvas.drawBitmap( icon, (float) (38 - (icon.getWidth() / 2.0)), (float) (38 - (icon.getHeight() / 2.0)), null ); } } widgetImages.add(AssetImageFactory.createAssetImage( widgetBitmap, true, w.getAngle(), w.getDistance(), 1 )); } else if (drawCircles) { widgetImages.add(AssetImageFactory.createAssetImage( circleBitmap, true, w.getAngle(), w.getDistance(), 1 )); } continue; } CustomWidget widget = (CustomWidget) w; Bitmap widgetBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888); Canvas widgetCanvas = new Canvas(widgetBitmap); if (drawCircles) { widgetCanvas.drawBitmap(circleBitmap, 0, 0, null); } for (CustomWidgetElement element : widget.getElements()) { if (element.getWidgetElementType() == CustomWidgetElement.WidgetElementType.TYPE_BACKGROUND) { File imageFile = new File(element.getValue()); if (!imageFile.exists() || !imageFile.isFile()) { LOG.debug("Image file " + element.getValue() + " not found"); continue; } Bitmap imageBitmap = BitmapFactory.decodeFile(element.getValue()); if (imageBitmap == null) { LOG.debug("image file " + element.getValue() + " could not be decoded"); continue; } Bitmap scaledBitmap = Bitmap.createScaledBitmap(imageBitmap, 76, 76, false); widgetCanvas.drawBitmap( scaledBitmap, 0, 0, null); break; } } for (CustomWidgetElement element : widget.getElements()) { if (element.getWidgetElementType() == CustomWidgetElement.WidgetElementType.TYPE_TEXT) { Paint textPaint = new Paint(); textPaint.setStrokeWidth(4); textPaint.setTextSize(17f); textPaint.setStyle(Paint.Style.FILL); textPaint.setColor(forceWhiteBackground ? Color.BLACK : Color.WHITE); textPaint.setTextAlign(Paint.Align.CENTER); widgetCanvas.drawText(element.getValue(), element.getX(), element.getY() - (textPaint.descent() + textPaint.ascent()) / 2f, textPaint); } else if (element.getWidgetElementType() == CustomWidgetElement.WidgetElementType.TYPE_IMAGE) { Bitmap imageBitmap = BitmapFactory.decodeFile(element.getValue()); widgetCanvas.drawBitmap(imageBitmap, element.getX() - imageBitmap.getWidth() / 2f, element.getY() - imageBitmap.getHeight() / 2f, null); } } widgetImages.add(AssetImageFactory.createAssetImage( widgetBitmap, true, widget.getAngle(), widget.getDistance(), 1 )); } AssetImage[] images = widgetImages.toArray(new AssetImage[0]); ArrayList pushFiles = new ArrayList<>(4); imgloop: for (AssetImage image : images) { for (AssetImage pushedImage : pushFiles) { // no need to send same file multiple times, filtering by name since name is hash if (image.getFileName().equals(pushedImage.getFileName())) continue imgloop; } pushFiles.add(image); } if (pushFiles.size() > 0) { queueWrite(new AssetFilePutRequest( pushFiles.toArray(new AssetImage[0]), FileHandle.ASSET_BACKGROUND_IMAGES, this )); } // queueWrite(new FileDeleteRequest((short) 0x0503)); queueWrite(new ImagesSetRequest( images, this )); } catch (IOException e) { LOG.error("Error while rendering widgets", e); } } private void handleFileDownload(String name, byte[] file, boolean toCache) { Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_DOWNLOADED_FILE); File outputFile = new File(getContext().getExternalFilesDir("download"), name + "_" + System.currentTimeMillis() + ".bin"); try { FileOutputStream fos = new FileOutputStream(outputFile); fos.write(file); fos.close(); resultIntent.putExtra("EXTRA_SUCCESS", true); resultIntent.putExtra("EXTRA_PATH", outputFile.getAbsolutePath()); resultIntent.putExtra("EXTRA_NAME", name); resultIntent.putExtra("EXTRA_TOCACHE", toCache); LOG.info("Wrote downloaded file to " + outputFile.getAbsolutePath()); } catch (IOException e) { LOG.error("Error while downloading file", e); resultIntent.putExtra("EXTRA_SUCCESS", false); } LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); } @Override public void uploadFileGenerateHeader(FileHandle handle, String filePath, boolean fileIsEncrypted) { final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE); byte[] fileData; try { FileInputStream fis = new FileInputStream(filePath); fileData = new byte[fis.available()]; fis.read(fileData); fis.close(); } catch (IOException e) { LOG.error("Error while reading file", e); resultIntent.putExtra("EXTRA_SUCCESS", false); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); return; } queueWrite(new FilePutRequest(handle, fileData, this) { @Override public void onFilePut(boolean success) { resultIntent.putExtra("EXTRA_SUCCESS", success); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); } }); } @Override public void uploadFileIncludesHeader(String filePath) { final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE); try { FileInputStream fis = new FileInputStream(filePath); uploadFileIncludesHeader(fis); fis.close(); } catch (Exception e) { LOG.error("Error while uploading file", e); resultIntent.putExtra("EXTRA_SUCCESS", false); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); } } private void uploadFileIncludesHeader(InputStream fis) throws IOException { final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE); byte[] fileData = new byte[fis.available()]; fis.read(fileData); short handleBytes = (short) (fileData[0] & 0xFF | ((fileData[1] & 0xFF) << 8)); FileHandle handle = FileHandle.fromHandle(handleBytes); if (handle == null) { throw new RuntimeException("unknown handle"); } queueWrite(new FilePutRawRequest(handle, fileData, this) { @Override public void onFilePut(boolean success) { resultIntent.putExtra("EXTRA_SUCCESS", success); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent); } }); if (handle == FileHandle.APP_CODE) { listApplications(); } } @Override public void downloadFile(byte majorHandle, byte minorHandle, String name, boolean fileIsEncrypted, boolean toCache) { if (fileIsEncrypted) { queueWrite((FileEncryptedInterface) new FileEncryptedGetRequest(majorHandle, minorHandle, this) { @Override public void handleFileData(byte[] fileData) { LOG.debug("downloaded encrypted file"); handleFileDownload(name, fileData, toCache); } }); } else { queueWrite(new FileGetRawRequest(majorHandle, minorHandle, this) { @Override public void handleFileRawData(byte[] fileData) { LOG.debug("downloaded regular file"); handleFileDownload(name, fileData, toCache); } }); } } @Override public void handleSetMenuStructure(JSONObject menuStructure) { String serialized = menuStructure.toString(); getDeviceSpecificPreferences() .edit() .putString("MENU_STRUCTURE_JSON", serialized) .apply(); try { String payload = new JSONObject() .put("push", new JSONObject() .put("set", new JSONObject() .put("customWatchFace._.config.menu_structure", menuStructure) ) ).toString(); pushConfigJson(payload); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void setWidgetContent(String widgetID, String content, boolean renderOnWatch) { boolean update = false; for (Widget widget : widgets) { if (!(widget instanceof CustomWidget)) continue; if (((CustomWidget) widget).updateElementValue(widgetID, content)) update = true; } if (renderOnWatch && update) renderWidgets(); } private void queueWrite(final FileEncryptedInterface request) { try { queueWrite(new VerifyPrivateKeyRequest( this.getSecretKey(), this ) { @Override protected void handleAuthenticationResult(boolean success) { if (success) { GB.log("success auth", GB.INFO, null); queueWrite((FossilRequest) request, true); } } }); } catch (IllegalAccessException e) { GB.toast("error getting key: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); } } @Override public void onInstallApp(Uri uri) { FossilFileReader fossilFile; try { fossilFile = new FossilFileReader(uri, getContext()); if (fossilFile.isFirmware()) { super.onInstallApp(uri); } else if (fossilFile.isApp() || fossilFile.isWatchface()) { UriHelper uriHelper = UriHelper.get(uri, getContext()); InputStream in = new BufferedInputStream(uriHelper.openInputStream()); uploadFileIncludesHeader(in); in.close(); } } catch (Exception ignored) { } } private void negotiateSymmetricKey() { try { queueWrite(new VerifyPrivateKeyRequest( this.getSecretKey(), this ) { @Override protected void handleAuthenticationResult(boolean success) { FossilHRWatchAdapter.this.handleAuthenticationResult(success); } }); } catch (IllegalAccessException e) { GB.toast("error getting key: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); this.handleAuthenticationResult(false); } } @Override public void setTime() { if (connectionMode == CONNECTION_MODE.NOT_AUTHENTICATED) { GB.toast(getContext().getString(R.string.fossil_hr_unavailable_unauthed), Toast.LENGTH_LONG, GB.ERROR); return; } queueWrite( (FileEncryptedInterface) new ConfigurationPutRequest(this.generateTimeConfigItemNow(), this) ); } @Override public void setMusicInfo(MusicSpec musicSpec) { musicSpec = new MusicSpec(musicSpec); if(musicSpec.album == null) musicSpec.album = ""; if(musicSpec.artist == null) musicSpec.artist = ""; if(musicSpec.track == null) musicSpec.track = ""; if ( currentSpec != null && currentSpec.album.equals(musicSpec.album) && currentSpec.artist.equals(musicSpec.artist) && currentSpec.track.equals(musicSpec.track) ) return; currentSpec = musicSpec; try { queueWrite(new MusicInfoSetRequest( musicSpec.artist, musicSpec.album, musicSpec.track, this )); } catch (BufferOverflowException e) { GB.log("musicInfo: " + musicSpec, GB.ERROR, e); } } @Override public void setMusicState(MusicStateSpec stateSpec) { super.setMusicState(stateSpec); queueWrite(new MusicControlRequest( stateSpec.state == MusicStateSpec.STATE_PLAYING ? MUSIC_PHONE_REQUEST.MUSIC_REQUEST_SET_PLAYING : MUSIC_PHONE_REQUEST.MUSIC_REQUEST_SET_PAUSED )); } @Override public void updateWidgets() { loadWidgets(); renderWidgets(); } @Override public void onFetchActivityData() { if (connectionMode == CONNECTION_MODE.NOT_AUTHENTICATED) { GB.toast(getContext().getString(R.string.fossil_hr_unavailable_unauthed), Toast.LENGTH_LONG, GB.ERROR); return; } syncSettings(); queueWrite(new FileLookupRequest(FileHandle.ACTIVITY_FILE, this) { @Override public void handleFileLookup(final short fileHandle) { queueWrite((FileEncryptedInterface) new FileEncryptedGetRequest(fileHandle, FossilHRWatchAdapter.this) { @Override public void handleFileData(byte[] fileData) { try (DBHandler dbHandler = GBApplication.acquireDB()) { ActivityFileParser parser = new ActivityFileParser(); ArrayList entries = parser.parseFile(fileData); HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession()); HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()]; Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); Long deviceId = DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession()).getId(); for (int i = 0; i < entries.size(); i++) { samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId); } provider.addGBActivitySamples(samples); if (saveRawActivityFiles) { writeFile(String.valueOf(System.currentTimeMillis()), fileData); } queueWrite(new FileDeleteRequest(fileHandle)); if (BuildConfig.DEBUG) GB.toast(getContext().getString(R.string.fossil_hr_synced_activity_data), Toast.LENGTH_SHORT, GB.INFO); } catch (Exception ex) { GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); } getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext()); } }); } @Override public void handleFileLookupError(FILE_LOOKUP_ERROR error) { if (error == FILE_LOOKUP_ERROR.FILE_EMPTY) { if (BuildConfig.DEBUG) GB.toast("activity file empty yet", Toast.LENGTH_LONG, GB.ERROR); } else { throw new RuntimeException("strange lookup stuff"); } getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext()); } }); } private void writeFile(String fileName, byte[] value) { File activityDir = new File(getContext().getExternalFilesDir(null), "activity_hr"); activityDir.mkdir(); File f = new File(activityDir, fileName); try { f.createNewFile(); FileOutputStream fos = new FileOutputStream(f); fos.write(value); fos.close(); GB.toast("saved file data", Toast.LENGTH_SHORT, GB.INFO); } catch (IOException e) { GB.log("file error", GB.ERROR, e); } } private void syncSettings() { if (connectionMode == CONNECTION_MODE.NOT_AUTHENTICATED) { GB.toast(getContext().getString(R.string.fossil_hr_unavailable_unauthed), Toast.LENGTH_LONG, GB.ERROR); return; } queueWrite((FileEncryptedInterface) new ConfigurationGetRequest(this)); } @Override public void setActivityHand(double progress) { // super.setActivityHand(progress); } private boolean isNotificationWidgetVisible() { for (Widget widget : widgets) { if (widget.getWidgetType() == Widget.WidgetType.LAST_NOTIFICATION) { return true; } } return false; } public boolean playRawNotification(NotificationSpec notificationSpec) { String sourceAppId = notificationSpec.sourceAppId; String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title); // Retrieve and store notification or app icon if (sourceAppId != null) { if (appIconCache.get(sourceAppId) == null) { try { Drawable icon = null; if (notificationSpec.iconId != 0) { Context sourcePackageContext = getContext().createPackageContext(sourceAppId, 0); icon = ResourcesCompat.getDrawable(sourcePackageContext.getResources(), notificationSpec.iconId, null); } if (icon == null) { icon = NotificationUtils.getAppIcon(getContext(), sourceAppId); } Bitmap iconBitmap = convertDrawableToBitmap(icon); appIconCache.put(sourceAppId, iconBitmap); setNotificationConfigurations(); } catch (PackageManager.NameNotFoundException e) { LOG.error("Error while updating notification icons", e); } } } boolean packageFound = false; // Send notification to watch try { for (NotificationHRConfiguration configuration : this.notificationConfigurations) { if (configuration.getPackageName().equals(sourceAppId)) { LOG.info("Package found in notificationConfigurations, using custom icon: " + sourceAppId); queueWrite(new PlayTextNotificationRequest(sourceAppId, senderOrTitle, notificationSpec, this)); packageFound = true; } } if (!packageFound) { LOG.info("Package not found in notificationConfigurations, using generic icon: " + sourceAppId); queueWrite(new PlayTextNotificationRequest("generic", senderOrTitle, notificationSpec, this)); } } catch (Exception e) { LOG.error("Error while forwarding notification", e); } // Update notification icon custom widget if (isNotificationWidgetVisible() && sourceAppId != null) { if (!sourceAppId.equals(this.lastPostedApp)) { this.lastPostedApp = sourceAppId; renderWidgets(); } } return true; } @Override public void onDeleteNotification(int id) { super.onDeleteNotification(id); // send notification dismissal message to watch try { queueWrite(new DismissTextNotificationRequest(id, this)); } catch (Exception e) { LOG.error("Error while dismissing notification", e); } // only delete app icon when no notification of said app is present for (String app : NotificationListener.notificationStack) { if (app.equals(this.lastPostedApp)) return; } this.lastPostedApp = null; if (isNotificationWidgetVisible()) { renderWidgets(); } } @Override public void onSetCallState(CallSpec callSpec) { super.onSetCallState(callSpec); String[] quickReplies = getQuickReplies(); boolean quickRepliesEnabled = quickReplies.length > 0 && callSpec.number != null && callSpec.number.matches("^\\+(?:[0-9] ?){6,14}[0-9]$"); if (callSpec.command == CallSpec.CALL_INCOMING) { currentCallSpec = callSpec; queueWrite(new PlayCallNotificationRequest(StringUtils.getFirstOf(callSpec.name, callSpec.number), true, quickRepliesEnabled, callSpec.dndSuppressed, this)); } else { currentCallSpec = null; queueWrite(new PlayCallNotificationRequest(StringUtils.getFirstOf(callSpec.name, callSpec.number), false, quickRepliesEnabled, callSpec.dndSuppressed, this)); } } // this method is based on the one from AppMessageHandlerYWeather.java private int getIconForConditionCode(int conditionCode, boolean isNight) { final int CLEAR_DAY = 0; final int CLEAR_NIGHT = 1; final int CLOUDY = 2; final int PARTLY_CLOUDY_DAY = 3; final int PARTLY_CLOUDY_NIGHT = 4; final int RAIN = 5; final int SNOW = 6; final int SNOW_2 = 7; // same as 6? final int THUNDERSTORM = 8; final int CLOUDY_2 = 9; // same as 2? final int WINDY = 10; if (conditionCode == 800 || conditionCode == 951) { return isNight ? CLEAR_NIGHT : CLEAR_DAY; } else if (conditionCode > 800 && conditionCode < 900) { return isNight ? PARTLY_CLOUDY_NIGHT : PARTLY_CLOUDY_DAY; } else if (conditionCode >= 300 && conditionCode < 400) { return RAIN; // drizzle mapped to rain } else if (conditionCode >= 500 && conditionCode < 600) { return RAIN; } else if (conditionCode >= 700 && conditionCode < 732) { return CLOUDY; } else if (conditionCode == 741 || conditionCode == 751 || conditionCode == 761 || conditionCode == 762) { return CLOUDY; // fog mapped to cloudy } else if (conditionCode == 771) { return CLOUDY; // squalls mapped to cloudy } else if (conditionCode == 781) { return WINDY; // tornato mapped to windy } else if (conditionCode >= 200 && conditionCode < 300) { return THUNDERSTORM; } else if (conditionCode >= 600 && conditionCode <= 602) { return SNOW; } else if (conditionCode >= 611 && conditionCode <= 622) { return RAIN; } else if (conditionCode == 906) { return RAIN; // hail mapped to rain } else if (conditionCode >= 907 && conditionCode < 957) { return WINDY; } else if (conditionCode == 905) { return WINDY; } else if (conditionCode == 900) { return WINDY; } else if (conditionCode == 901 || conditionCode == 902 || conditionCode == 962) { return WINDY; } return isNight ? CLEAR_NIGHT : CLEAR_DAY; } @Override public void onSendWeather(WeatherSpec weatherSpec) { boolean isNight; if (weatherSpec.sunRise != 0 && weatherSpec.sunSet != 0) { isNight = weatherSpec.sunRise * 1000L > System.currentTimeMillis() || weatherSpec.sunSet * 1000L < System.currentTimeMillis(); } else { Location location = weatherSpec.getLocation(); if (location == null) { location = new CurrentPosition().getLastKnownLocation(); } GregorianCalendar[] sunrise = SPA.calculateSunriseTransitSet(new GregorianCalendar(), location.getLatitude(), location.getLongitude(), DeltaT.estimate(new GregorianCalendar())); isNight = sunrise[0].getTimeInMillis() > System.currentTimeMillis() || sunrise[2].getTimeInMillis() < System.currentTimeMillis(); } long ts = System.currentTimeMillis(); ts /= 1000; try { JSONObject responseObject = new JSONObject() .put("res", new JSONObject() .put("id", 0) // seems the id does not matter? .put("set", new JSONObject() .put("weatherInfo", new JSONObject() .put("alive", ts + 60 * 60) .put("unit", "c") // FIXME: do not hardcode .put("temp", weatherSpec.currentTemp - 273) .put("cond_id", getIconForConditionCode(weatherSpec.currentConditionCode, isNight)) ) ) ); queueWrite(new JsonPutRequest(responseObject, this)); JSONArray forecastWeekArray = new JSONArray(); final String[] weekdays = {"", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(weatherSpec.timestamp * 1000L); int i = 0; for (WeatherSpec.Daily forecast : weatherSpec.forecasts) { cal.add(Calendar.DATE, 1); int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); forecastWeekArray.put(new JSONObject() .put("day", weekdays[dayOfWeek]) .put("cond_id", getIconForConditionCode(forecast.conditionCode, false)) .put("high", forecast.maxTemp - 273) .put("low", forecast.minTemp - 273) ); if (++i == 3) break; // max 3 } JSONArray forecastDayArray = new JSONArray(); final int[] hours = {0, 0, 0}; for (int hour : hours) { forecastDayArray.put(new JSONObject() .put("hour", hour) .put("cond_id", 0) .put("temp", 0) ); } JSONObject forecastResponseObject = new JSONObject() .put("res", new JSONObject() .put("id", 0) .put("set", new JSONObject() .put("weatherApp._.config.locations", new JSONArray() .put(new JSONObject() .put("alive", ts + 60 * 60) .put("city", weatherSpec.location) .put("unit", "c") // FIXME: do not hardcode .put("temp", weatherSpec.currentTemp - 273) .put("high", weatherSpec.todayMaxTemp - 273) .put("low", weatherSpec.todayMinTemp - 273) .put("rain", weatherSpec.precipProbability) .put("uv", Math.round(weatherSpec.uvIndex)) .put("message", weatherSpec.currentCondition) .put("cond_id", getIconForConditionCode(weatherSpec.currentConditionCode, isNight)) .put("forecast_day", forecastDayArray) .put("forecast_week", forecastWeekArray) ) ) ) ); queueWrite(new JsonPutRequest(forecastResponseObject, this)); } catch (JSONException e) { LOG.error("JSON exception: ", e); } } public void onSendChanceOfRain(WeatherSpec weatherSpec) { long ts = System.currentTimeMillis(); ts /= 1000; try { JSONObject rainObject = new JSONObject() .put("res", new JSONObject() .put("set", new JSONObject() .put("widgetChanceOfRain._.config.info", new JSONObject() .put("alive", ts + 60 * 15) .put("rain", weatherSpec.precipProbability) ) ) ); queueWrite(new JsonPutRequest(rainObject, this)); } catch (JSONException e) { LOG.error("JSON exception: ", e); } } public void onSendUVIndex(WeatherSpec weatherSpec) { long ts = System.currentTimeMillis(); ts /= 1000; try { JSONObject rainObject = new JSONObject() .put("res", new JSONObject() .put("set", new JSONObject() .put("widgetUV._.config.info", new JSONObject() .put("alive", ts + 60 * 15) .put("uv", Math.round(weatherSpec.uvIndex)) ) ) ); queueWrite(new JsonPutRequest(rainObject, this)); } catch (JSONException e) { LOG.error("JSON exception: ", e); } } @Override public void factoryReset() { queueWrite(new FactoryResetRequest()); } @Override public void onTestNewFunction() { setVibrationStrengthFromConfig(); } public byte[] getSecretKey() throws IllegalAccessException { byte[] authKeyBytes = new byte[16]; SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(getDeviceSupport().getDevice().getAddress()); String authKey = sharedPrefs.getString("authkey", null); if (authKey != null && !authKey.isEmpty()) { authKey = authKey.replace(" ", ""); authKey = authKey.replace("0x", ""); if (authKey.length() != 32) { throw new IllegalAccessException("Key should be 16 bytes long as hex string"); } byte[] srcBytes = GB.hexStringToByteArray(authKey); System.arraycopy(srcBytes, 0, authKeyBytes, 0, Math.min(srcBytes.length, 16)); } return authKeyBytes; } @Override public void pushConfigJson(String configJson) { configJson = configJson.replace("\n", ""); queueWrite(new JsonPutRequest(configJson, this)); } public void setPhoneRandomNumber(byte[] phoneRandomNumber) { this.phoneRandomNumber = phoneRandomNumber; } public byte[] getPhoneRandomNumber() { return phoneRandomNumber; } public void setWatchRandomNumber(byte[] watchRandomNumber) { this.watchRandomNumber = watchRandomNumber; } public byte[] getWatchRandomNumber() { return watchRandomNumber; } @Override public void overwriteButtons(String jsonConfigString) { try { SharedPreferences prefs = getDeviceSpecificPreferences(); String singlePressEvent = "short_press_release"; Version firmwareVersion = getCleanFWVersion(); if (firmwareVersion != null && firmwareVersion.smallerThan(new Version("2.19"))) { singlePressEvent = "single_click"; } ArrayList configs = new ArrayList<>(6); configs.add(new ButtonConfiguration("top_" + singlePressEvent, prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_SHORT, "weatherApp"))); configs.add(new ButtonConfiguration("top_hold", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_LONG, "weatherApp"))); // configs.add(new ButtonConfiguration("top_double_click", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_DOUBLE, "weatherApp"))); configs.add(new ButtonConfiguration("middle_" + singlePressEvent, prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_SHORT, "commuteApp"))); if (firmwareVersion != null && firmwareVersion.greaterOrEqualThan(new Version("3.0"))) { configs.add(new ButtonConfiguration("middle_hold", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_LONG, "launcherApp"))); } // configs.add(new ButtonConfiguration("middle_double_click", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_DOUBLE, "commuteApp"))); configs.add(new ButtonConfiguration("bottom_" + singlePressEvent, prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_SHORT, "musicApp"))); configs.add(new ButtonConfiguration("bottom_hold", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_LONG, "musicApp"))); // configs.add(new ButtonConfiguration("bottom_double_click", prefs.getString(DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_DOUBLE, "musicApp"))); // filter out all apps not installed on watch ArrayList availableConfigs = new ArrayList<>(); outerLoop: for (ButtonConfiguration config : configs) { for (ApplicationInformation installedApp : installedApplications) { if (installedApp.getAppName().equals(config.getAction()) || config.getAction().equals("workoutApp") //workoutApp is part of internal firmware ) { availableConfigs.add(config); continue outerLoop; } } } queueWrite(new ButtonConfigurationPutRequest( availableConfigs.toArray(new ButtonConfiguration[0]), this )); for (ApplicationInformation info : installedApplications) { if (info.getAppName().equals("commuteApp")) { JSONArray jsonArray = new JSONArray( GBApplication.getPrefs().getString(CommuteActionsActivity.CONFIG_KEY_Q_ACTIONS, "[]") ); String[] menuItems = new String[jsonArray.length()]; for (int i = 0; i < jsonArray.length(); i++) menuItems[i] = jsonArray.getString(i); queueWrite(new CommuteConfigPutRequest(menuItems, this)); break; } } } catch (JSONException e) { LOG.error("Error while configuring buttons", e); } } private void setActivityRecognition(){ SharedPreferences prefs = getDeviceSpecificPreferences(); boolean runningEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_ENABLED, false); boolean runningAskFirst = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_ASK_FIRST, false); int runningMinutes = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_MINUTES, "3")); boolean bikingEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_ENABLED, false); boolean bikingAskFirst = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_ASK_FIRST, false); int bikingMinutes = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_MINUTES, "5")); boolean walkingEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_ENABLED, false); boolean walkingAskFirst = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_ASK_FIRST, false); int walkingMinutes = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_MINUTES, "10")); boolean rowingEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_ENABLED, false); boolean rowingAskFirst = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_ASK_FIRST, false); int rowingMinutes = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_MINUTES, "3")); if (runningMinutes < 1) runningMinutes = 1; if (runningMinutes > 255) runningMinutes = 255; if (bikingMinutes < 1) bikingMinutes = 1; if (bikingMinutes > 255) bikingMinutes = 255; if (walkingMinutes < 1) walkingMinutes = 1; if (walkingMinutes > 255) walkingMinutes = 255; if (rowingMinutes < 1) rowingMinutes = 1; if (rowingMinutes > 255) rowingMinutes = 255; FitnessConfigItem fitnessConfigItem = new FitnessConfigItem( runningEnabled, runningAskFirst, runningMinutes, bikingEnabled, bikingAskFirst, bikingMinutes, walkingEnabled, walkingAskFirst, walkingMinutes, rowingEnabled, rowingAskFirst, rowingMinutes ); queueWrite((FileEncryptedInterface) new ConfigurationPutRequest(fitnessConfigItem, this)); } private void setInactivityWarning(){ SharedPreferences prefs = getDeviceSpecificPreferences(); boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE, false); int threshold = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, "60")); String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, "06:00"); String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, "22:00"); int startHour = Integer.parseInt(start.split(":")[0]); int startMinute = Integer.parseInt(start.split(":")[1]); int endHour = Integer.parseInt(end.split(":")[0]); int endMinute = Integer.parseInt(end.split(":")[1]); InactivityWarningItem inactivityWarningItem = new InactivityWarningItem( startHour, startMinute, endHour, endMinute, threshold, enabled ); queueWrite((FileEncryptedInterface) new ConfigurationPutRequest(inactivityWarningItem, this)); } @Override public void onSendConfiguration(String config) { switch (config) { case DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_SHORT: case DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_SHORT: case DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_SHORT: case DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_LONG: case DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_LONG: case DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_LONG: case DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION_DOUBLE: case DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION_DOUBLE: case DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION_DOUBLE: overwriteButtons(null); break; case DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE: setVibrationStrengthFromConfig(); break; case "force_white_color_scheme": loadBackground(); // not break here case "widget_draw_circles": { renderWidgets(); break; } case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES: { saveRawActivityFiles = getDeviceSpecificPreferences().getBoolean("save_raw_activity_files", false); break; } case SettingsActivity.PREF_MEASUREMENT_SYSTEM: setUnitsConfig(); break; case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_ENABLED: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_ASK_FIRST: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_RUNNING_MINUTES: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_ENABLED: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_ASK_FIRST: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_BIKING_MINUTES: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_ENABLED: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_ASK_FIRST: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_WALKING_MINUTES: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_ENABLED: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_ASK_FIRST: case DeviceSettingsPreferenceConst.PREF_HYBRID_HR_ACTIVITY_RECOGNITION_ROWING_MINUTES: setActivityRecognition(); break; case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_START: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_END: setInactivityWarning(); break; } } @Override public void handleHeartRateCharacteristic(BluetoothGattCharacteristic characteristic) { super.handleHeartRateCharacteristic(characteristic); byte[] value = characteristic.getValue(); int heartRate = value[1]; LOG.debug("heart rate: " + heartRate); } @Override protected void handleBackgroundCharacteristic(BluetoothGattCharacteristic characteristic) { super.handleBackgroundCharacteristic(characteristic); byte[] value = characteristic.getValue(); byte requestType = value[1]; if (requestType == (byte) 0x04) { if (value[7] == 0x00 || value[7] == 0x01) { handleCallRequest(value); } else if (value[7] == 0x02) { handleDeleteNotification(value); } else if (value[7] == 0x03) { handleQuickReplyRequest(value); } } else if (requestType == (byte) 0x05) { handleMusicRequest(value); } else if (requestType == (byte) 0x01) { int eventId = value[2]; LOG.info("got event id " + eventId); try { String jsonString = new String(value, 3, value.length - 3); // logger.info(jsonString); JSONObject requestJson = new JSONObject(jsonString); JSONObject request = requestJson.getJSONObject("req"); int requestId = request.getInt("id"); if (request.has("ringMyPhone")) { String action = request.getJSONObject("ringMyPhone").getString("action"); LOG.info("got ringMyPhone request; " + action); GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); JSONObject responseObject = new JSONObject() .put("res", new JSONObject() .put("id", requestId) .put("set", new JSONObject() .put("ringMyPhone", new JSONObject() ) ) ); if ("on".equals(action)) { findPhoneEvent.event = GBDeviceEventFindPhone.Event.START; getDeviceSupport().evaluateGBDeviceEvent(findPhoneEvent); responseObject .getJSONObject("res") .getJSONObject("set") .getJSONObject("ringMyPhone") .put("result", "on"); queueWrite(new JsonPutRequest(responseObject, this)); } else if ("off".equals(action)) { findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP; getDeviceSupport().evaluateGBDeviceEvent(findPhoneEvent); responseObject .getJSONObject("res") .getJSONObject("set") .getJSONObject("ringMyPhone") .put("result", "off"); queueWrite(new JsonPutRequest(responseObject, this)); } } else if (request.has("weatherInfo") || request.has("weatherApp._.config.locations")) { LOG.info("Got weatherInfo request"); WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); if (weatherSpec != null) { onSendWeather(weatherSpec); } else { LOG.info("no weather data available - ignoring request"); } } else if (request.has("widgetChanceOfRain._.config.info")) { LOG.info("Got widgetChanceOfRain request"); WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); if (weatherSpec != null) { onSendChanceOfRain(weatherSpec); } else { LOG.info("no weather data available - ignoring request"); } } else if (request.has("widgetUV._.config.info")) { LOG.info("Got widgetUV request"); WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); if (weatherSpec != null) { onSendUVIndex(weatherSpec); } else { LOG.info("no weather data available - ignoring request"); } } else if (request.has("commuteApp._.config.commute_info")) { String action = request.getJSONObject("commuteApp._.config.commute_info") .getString("dest"); String startStop = request.getJSONObject("commuteApp._.config.commute_info") .getString("action"); if (startStop.equals("stop")) { // overwriteButtons(null); return; } Intent menuIntent = new Intent(QHybridSupport.QHYBRID_EVENT_COMMUTE_MENU); menuIntent.putExtra("EXTRA_ACTION", action); getContext().sendBroadcast(menuIntent); } else if (request.has("master._.config.app_status")) { queueWrite(new ConfirmAppStatusRequest(requestId, this)); } else if (request.has("workoutApp")) { JSONObject workoutRequest = request.getJSONObject("workoutApp"); String workoutState = workoutRequest.optString("state"); String workoutType = workoutRequest.optString("type"); LOG.info("Got workoutApp request, state=" + workoutState + ", type=" + workoutType); JSONObject workoutResponse = WorkoutRequestHandler.handleRequest(getContext(), requestId, workoutRequest); if (workoutResponse.length() > 0) { JSONObject responseObject = new JSONObject() .put("res", new JSONObject() .put("id", requestId) .put("set", workoutResponse) ); queueWrite(new JsonPutRequest(responseObject, this)); } } else if (request.optString("custom_menu").equals("request_config")) { PackageManager manager = getContext().getPackageManager(); try{ // only show toast when companion app is installed manager.getApplicationInfo("d.d.hrmenucompanion", 0); GB.toast(getContext().getString(R.string.info_fossil_rebuild_watchface_custom_menu), Toast.LENGTH_SHORT, GB.INFO); }catch (PackageManager.NameNotFoundException e){ } } else { LOG.warn("Unhandled request from watch: " + requestJson.toString()); } } catch (JSONException e) { LOG.error("Error while handling received characteristic", e); } } } @Override public void onFindDevice(boolean start) { super.onFindDevice(start); boolean versionSupportsConfirmation = getCleanFWVersion().greaterOrEqualThan(new Version("2.22")); if(!versionSupportsConfirmation){ GB.toast("not supported in this version", Toast.LENGTH_SHORT, GB.ERROR); return; } if (start) { queueWrite(new ConfirmOnDeviceRequest()); } } private void handleDeleteNotification(byte[] value) { ByteBuffer buffer = ByteBuffer.wrap(value); buffer.order(ByteOrder.LITTLE_ENDIAN); int notifId = buffer.getInt(3); Intent deleteIntent = new Intent(NotificationListener.ACTION_DISMISS); deleteIntent.putExtra("handle", (long) notifId); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(deleteIntent); } private void handleCallRequest(byte[] value) { SharedPreferences prefs = getDeviceSpecificPreferences(); String rejectMethodPref = prefs.getString(DeviceSettingsPreferenceConst.PREF_CALL_REJECT_METHOD, "reject"); GBDeviceEventCallControl.Event rejectMethod = GBDeviceEventCallControl.Event.REJECT; if (rejectMethodPref.equals("ignore")) rejectMethod = GBDeviceEventCallControl.Event.IGNORE; boolean acceptCall = value[7] == (byte) 0x00; queueWrite(new PlayCallNotificationRequest("", false, false, 0,this)); GBDeviceEventCallControl callControlEvent = new GBDeviceEventCallControl(); callControlEvent.event = acceptCall ? GBDeviceEventCallControl.Event.START : rejectMethod; getDeviceSupport().evaluateGBDeviceEvent(callControlEvent); } private void handleQuickReplyRequest(byte[] value) { if (currentCallSpec == null) { return; } String[] quickReplies = getQuickReplies(); byte callId = value[3]; byte replyChoice = value[8]; if (replyChoice >= quickReplies.length) { return; } GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl(); devEvtNotificationControl.handle = callId; devEvtNotificationControl.phoneNumber = currentCallSpec.number; devEvtNotificationControl.reply = quickReplies[replyChoice]; devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; getDeviceSupport().evaluateGBDeviceEvent(devEvtNotificationControl); queueWrite(new QuickReplyConfirmationPutRequest(callId)); } private void handleMusicRequest(byte[] value) { byte command = value[3]; LOG.info("got music command: " + command); MUSIC_WATCH_REQUEST request = MUSIC_WATCH_REQUEST.fromCommandByte(command); GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY; // TODO add skipping/seeking switch (request) { case MUSIC_REQUEST_PLAY_PAUSE: { queueWrite(new MusicControlRequest(MUSIC_PHONE_REQUEST.MUSIC_REQUEST_PLAY_PAUSE)); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAYPAUSE; break; } case MUSIC_REQUEST_NEXT: { queueWrite(new MusicControlRequest(MUSIC_PHONE_REQUEST.MUSIC_REQUEST_NEXT)); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT; break; } case MUSIC_REQUEST_PREVIOUS: { queueWrite(new MusicControlRequest(MUSIC_PHONE_REQUEST.MUSIC_REQUEST_PREVIOUS)); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS; break; } case MUSIC_REQUEST_LOUDER: { queueWrite(new MusicControlRequest(MUSIC_PHONE_REQUEST.MUSIC_REQUEST_LOUDER)); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP; break; } case MUSIC_REQUEST_QUITER: { queueWrite(new MusicControlRequest(MUSIC_PHONE_REQUEST.MUSIC_REQUEST_QUITER)); deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; break; } } getDeviceSupport().evaluateGBDeviceEvent(deviceEventMusicControl); } @Override public void setCommuteMenuMessage(String message, boolean finished) { queueWrite(new SetCommuteMenuMessage(message, finished, this)); } public byte getJsonIndex() { return jsonIndex++; } private Version getCleanFWVersion() { return new Version(getDeviceSupport().getDevice().getFirmwareVersion2()); } public String getInstalledAppNameFromUUID(UUID uuid) { for (ApplicationInformation appInfo : installedApplications) { if (UUID.nameUUIDFromBytes(appInfo.getAppName().getBytes(StandardCharsets.UTF_8)).equals(uuid)) { return appInfo.getAppName(); } } if (uuid.equals(UUID.nameUUIDFromBytes("workoutApp".getBytes(StandardCharsets.UTF_8)))) { return "workoutApp"; } return null; } public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) { SharedPreferences prefs = getDeviceSpecificPreferences(); String installedAppsJson = getDeviceSupport().getDevice().getDeviceInfo("INSTALLED_APPS").getDetails(); if (installedAppsJson == null || !installedAppsJson.contains("navigationApp")) { if (!notifiedAboutMissingNavigationApp) { notifiedAboutMissingNavigationApp = true; NotificationCompat.Builder ncomp = new NotificationCompat.Builder(getContext(), NOTIFICATION_CHANNEL_ID) .setContentTitle(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_title)) .setContentText(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text)) .setTicker(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text)) .setSmallIcon(R.drawable.ic_notification) .setAutoCancel(true); GB.notify((int) System.currentTimeMillis(), ncomp.build(), getContext()); GB.toast(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text), Toast.LENGTH_LONG, GB.WARN); } return; } try { JSONObject navJson = new JSONObject() .put("push", new JSONObject() .put("set", new JSONObject() .put("navigationApp._.config.info", new JSONObject() .put("distance", navigationInfoSpec.distanceToTurn) .put("eta", navigationInfoSpec.ETA) .put("instruction", navigationInfoSpec.instruction) .put("nextAction", navigationInfoSpec.nextAction) .put("autoFg", prefs.getBoolean("fossil_hr_nav_auto_foreground", true)) .put("vibrate", prefs.getBoolean("fossil_hr_nav_vibrate", true)) ) ) ); queueWrite(new JsonPutRequest(navJson, this)); } catch (JSONException e) { LOG.error("JSON exception: ", e); } } private void updateBuiltinAppsInCache() { FossilFileReader fileReader; try { fileReader = new FossilFileReader(FileUtils.getUriForAsset("fossil_hr/navigationApp.wapp", getContext()), getContext()); if (FossilHRInstallHandler.saveAppInCache(fileReader, fileReader.getBackground(), fileReader.getPreview(), getDeviceSupport().getDevice().getDeviceCoordinator(), getContext())) { LOG.info("Successfully copied navigationApp for Fossil Hybrids to cache"); } } catch (IOException e) { LOG.warn("Could not copy navigationApp to cache", e); } } }