2675 lines
112 KiB
Java
2675 lines
112 KiB
Java
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
|
Gobbetti, Frank Slezak, jcrode, Johann C. Rode, Julien Pivotto, Kevin Richter,
|
|
Sergio Lopez, Steffen Liebergeld, Uwe Hermann
|
|
|
|
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 <http://www.gnu.org/licenses/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
|
|
|
|
import android.util.Base64;
|
|
import android.util.Pair;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
import java.util.SimpleTimeZone;
|
|
import java.util.UUID;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppManagement;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.pebble.GBDeviceEventDataLogging;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec.Action;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public class PebbleProtocol extends GBDeviceProtocol {
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(PebbleProtocol.class);
|
|
|
|
private static final short ENDPOINT_TIME = 11;
|
|
private static final short ENDPOINT_FIRMWAREVERSION = 16;
|
|
private static final short ENDPOINT_PHONEVERSION = 17;
|
|
private static final short ENDPOINT_SYSTEMMESSAGE = 18;
|
|
private static final short ENDPOINT_MUSICCONTROL = 32;
|
|
private static final short ENDPOINT_PHONECONTROL = 33;
|
|
static final short ENDPOINT_APPLICATIONMESSAGE = 48;
|
|
private static final short ENDPOINT_LAUNCHER = 49;
|
|
private static final short ENDPOINT_APPRUNSTATE = 52; // FW >=3.x
|
|
private static final short ENDPOINT_LOGS = 2000;
|
|
private static final short ENDPOINT_PING = 2001;
|
|
private static final short ENDPOINT_LOGDUMP = 2002;
|
|
private static final short ENDPOINT_RESET = 2003;
|
|
private static final short ENDPOINT_APP = 2004;
|
|
private static final short ENDPOINT_APPLOGS = 2006;
|
|
private static final short ENDPOINT_NOTIFICATION = 3000; // FW 1.x-2-x
|
|
private static final short ENDPOINT_EXTENSIBLENOTIFS = 3010; // FW 2.x
|
|
private static final short ENDPOINT_RESOURCE = 4000;
|
|
private static final short ENDPOINT_SYSREG = 5000;
|
|
private static final short ENDPOINT_FCTREG = 5001;
|
|
private static final short ENDPOINT_APPMANAGER = 6000;
|
|
private static final short ENDPOINT_APPFETCH = 6001; // FW >=3.x
|
|
private static final short ENDPOINT_DATALOG = 6778;
|
|
private static final short ENDPOINT_RUNKEEPER = 7000;
|
|
private static final short ENDPOINT_SCREENSHOT = 8000;
|
|
private static final short ENDPOINT_AUDIOSTREAM = 10000;
|
|
private static final short ENDPOINT_VOICECONTROL = 11000;
|
|
private static final short ENDPOINT_NOTIFICATIONACTION = 11440; // FW >=3.x, TODO: find a better name
|
|
private static final short ENDPOINT_APPREORDER = (short) 0xabcd; // FW >=3.x
|
|
private static final short ENDPOINT_BLOBDB = (short) 0xb1db; // FW >=3.x
|
|
private static final short ENDPOINT_PUTBYTES = (short) 0xbeef;
|
|
|
|
private static final byte APPRUNSTATE_START = 1;
|
|
private static final byte APPRUNSTATE_STOP = 2;
|
|
|
|
private static final byte BLOBDB_INSERT = 1;
|
|
private static final byte BLOBDB_DELETE = 4;
|
|
private static final byte BLOBDB_CLEAR = 5;
|
|
|
|
private static final byte BLOBDB_PIN = 1;
|
|
private static final byte BLOBDB_APP = 2;
|
|
private static final byte BLOBDB_REMINDER = 3;
|
|
private static final byte BLOBDB_NOTIFICATION = 4;
|
|
private static final byte BLOBDB_WEATHER = 5;
|
|
private static final byte BLOBDB_CANNED_MESSAGES = 6;
|
|
private static final byte BLOBDB_PREFERENCES = 7;
|
|
private static final byte BLOBDB_APPSETTINGS = 9;
|
|
private static final byte BLOBDB_APPGLANCE = 11;
|
|
|
|
private static final byte BLOBDB_SUCCESS = 1;
|
|
private static final byte BLOBDB_GENERALFAILURE = 2;
|
|
private static final byte BLOBDB_INVALIDOPERATION = 3;
|
|
private static final byte BLOBDB_INVALIDDATABASEID = 4;
|
|
private static final byte BLOBDB_INVALIDDATA = 5;
|
|
private static final byte BLOBDB_KEYDOESNOTEXIST = 6;
|
|
private static final byte BLOBDB_DATABASEFULL = 7;
|
|
private static final byte BLOBDB_DATASTALE = 8;
|
|
|
|
|
|
private static final byte NOTIFICATION_EMAIL = 0;
|
|
private static final byte NOTIFICATION_SMS = 1;
|
|
private static final byte NOTIFICATION_TWITTER = 2;
|
|
private static final byte NOTIFICATION_FACEBOOK = 3;
|
|
|
|
private static final byte PHONECONTROL_ANSWER = 1;
|
|
private static final byte PHONECONTROL_HANGUP = 2;
|
|
private static final byte PHONECONTROL_GETSTATE = 3;
|
|
private static final byte PHONECONTROL_INCOMINGCALL = 4;
|
|
private static final byte PHONECONTROL_OUTGOINGCALL = 5;
|
|
private static final byte PHONECONTROL_MISSEDCALL = 6;
|
|
private static final byte PHONECONTROL_RING = 7;
|
|
private static final byte PHONECONTROL_START = 8;
|
|
private static final byte PHONECONTROL_END = 9;
|
|
|
|
private static final byte MUSICCONTROL_SETMUSICINFO = 0x10;
|
|
private static final byte MUSICCONTROL_SETPLAYSTATE = 0x11;
|
|
|
|
private static final byte MUSICCONTROL_PLAYPAUSE = 1;
|
|
private static final byte MUSICCONTROL_PAUSE = 2;
|
|
private static final byte MUSICCONTROL_PLAY = 3;
|
|
private static final byte MUSICCONTROL_NEXT = 4;
|
|
private static final byte MUSICCONTROL_PREVIOUS = 5;
|
|
private static final byte MUSICCONTROL_VOLUMEUP = 6;
|
|
private static final byte MUSICCONTROL_VOLUMEDOWN = 7;
|
|
private static final byte MUSICCONTROL_GETNOWPLAYING = 8;
|
|
|
|
private static final byte MUSICCONTROL_STATE_PAUSED = 0x00;
|
|
private static final byte MUSICCONTROL_STATE_PLAYING = 0x01;
|
|
private static final byte MUSICCONTROL_STATE_REWINDING = 0x02;
|
|
private static final byte MUSICCONTROL_STATE_FASTWORWARDING = 0x03;
|
|
private static final byte MUSICCONTROL_STATE_UNKNOWN = 0x04;
|
|
|
|
private static final byte NOTIFICATIONACTION_ACK = 0;
|
|
private static final byte NOTIFICATIONACTION_NACK = 1;
|
|
private static final byte NOTIFICATIONACTION_INVOKE = 0x02;
|
|
private static final byte NOTIFICATIONACTION_RESPONSE = 0x11;
|
|
|
|
private static final byte TIME_GETTIME = 0;
|
|
private static final byte TIME_SETTIME = 2;
|
|
private static final byte TIME_SETTIME_UTC = 3;
|
|
|
|
private static final byte FIRMWAREVERSION_GETVERSION = 0;
|
|
|
|
private static final byte APPMANAGER_GETAPPBANKSTATUS = 1;
|
|
private static final byte APPMANAGER_REMOVEAPP = 2;
|
|
private static final byte APPMANAGER_REFRESHAPP = 3;
|
|
private static final byte APPMANAGER_GETUUIDS = 5;
|
|
|
|
private static final int APPMANAGER_RES_SUCCESS = 1;
|
|
|
|
private static final byte APPLICATIONMESSAGE_PUSH = 1;
|
|
private static final byte APPLICATIONMESSAGE_REQUEST = 2;
|
|
private static final byte APPLICATIONMESSAGE_ACK = (byte) 0xff;
|
|
private static final byte APPLICATIONMESSAGE_NACK = (byte) 0x7f;
|
|
|
|
private static final byte DATALOG_OPENSESSION = 0x01;
|
|
private static final byte DATALOG_SENDDATA = 0x02;
|
|
private static final byte DATALOG_CLOSE = 0x03;
|
|
private static final byte DATALOG_TIMEOUT = 0x07;
|
|
private static final byte DATALOG_REPORTSESSIONS = (byte) 0x84;
|
|
private static final byte DATALOG_ACK = (byte) 0x85;
|
|
private static final byte DATALOG_NACK = (byte) 0x86;
|
|
|
|
private static final byte PING_PING = 0;
|
|
private static final byte PING_PONG = 1;
|
|
|
|
private static final byte PUTBYTES_INIT = 1;
|
|
private static final byte PUTBYTES_SEND = 2;
|
|
private static final byte PUTBYTES_COMMIT = 3;
|
|
private static final byte PUTBYTES_ABORT = 4;
|
|
private static final byte PUTBYTES_COMPLETE = 5;
|
|
|
|
public static final byte PUTBYTES_TYPE_FIRMWARE = 1;
|
|
public static final byte PUTBYTES_TYPE_RECOVERY = 2;
|
|
public static final byte PUTBYTES_TYPE_SYSRESOURCES = 3;
|
|
public static final byte PUTBYTES_TYPE_RESOURCES = 4;
|
|
public static final byte PUTBYTES_TYPE_BINARY = 5;
|
|
public static final byte PUTBYTES_TYPE_FILE = 6;
|
|
public static final byte PUTBYTES_TYPE_WORKER = 7;
|
|
|
|
private static final byte RESET_REBOOT = 0;
|
|
|
|
private static final byte SCREENSHOT_TAKE = 0;
|
|
|
|
private static final byte SYSTEMMESSAGE_NEWFIRMWAREAVAILABLE = 0;
|
|
private static final byte SYSTEMMESSAGE_FIRMWARESTART = 1;
|
|
private static final byte SYSTEMMESSAGE_FIRMWARECOMPLETE = 2;
|
|
private static final byte SYSTEMMESSAGE_FIRMWAREFAIL = 3;
|
|
private static final byte SYSTEMMESSAGE_FIRMWARE_UPTODATE = 4;
|
|
private static final byte SYSTEMMESSAGE_FIRMWARE_OUTOFDATE = 5;
|
|
private static final byte SYSTEMMESSAGE_STOPRECONNECTING = 6;
|
|
private static final byte SYSTEMMESSAGE_STARTRECONNECTING = 7;
|
|
|
|
private static final byte PHONEVERSION_REQUEST = 0;
|
|
private static final byte PHONEVERSION_APPVERSION_MAGIC = 2; // increase this if pebble complains
|
|
private static final byte PHONEVERSION_APPVERSION_MAJOR = 2;
|
|
private static final byte PHONEVERSION_APPVERSION_MINOR = 3;
|
|
private static final byte PHONEVERSION_APPVERSION_PATCH = 0;
|
|
|
|
|
|
private static final int PHONEVERSION_SESSION_CAPS_GAMMARAY = 0x80000000;
|
|
|
|
private static final int PHONEVERSION_REMOTE_CAPS_TELEPHONY = 0x00000010;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_SMS = 0x00000020;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_GPS = 0x00000040;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_BTLE = 0x00000080;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_REARCAMERA = 0x00000100;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_ACCEL = 0x00000200;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_GYRO = 0x00000400;
|
|
private static final int PHONEVERSION_REMOTE_CAPS_COMPASS = 0x00000800;
|
|
|
|
private static final byte PHONEVERSION_REMOTE_OS_UNKNOWN = 0;
|
|
private static final byte PHONEVERSION_REMOTE_OS_IOS = 1;
|
|
private static final byte PHONEVERSION_REMOTE_OS_ANDROID = 2;
|
|
private static final byte PHONEVERSION_REMOTE_OS_OSX = 3;
|
|
private static final byte PHONEVERSION_REMOTE_OS_LINUX = 4;
|
|
private static final byte PHONEVERSION_REMOTE_OS_WINDOWS = 5;
|
|
|
|
static final byte TYPE_BYTEARRAY = 0;
|
|
private static final byte TYPE_CSTRING = 1;
|
|
static final byte TYPE_UINT = 2;
|
|
static final byte TYPE_INT = 3;
|
|
|
|
private final short LENGTH_PREFIX = 4;
|
|
|
|
private static final byte LENGTH_UUID = 16;
|
|
|
|
private static final long GB_UUID_MASK = 0x4767744272646700L;
|
|
|
|
// base is -8
|
|
private static final String[] hwRevisions = {
|
|
// Emulator
|
|
"silk_bb2", "robert_bb", "silk_bb",
|
|
"spalding_bb2", "snowy_bb2", "snowy_bb",
|
|
"bb2", "bb",
|
|
"unknown",
|
|
// Pebble Classic Series
|
|
"ev1", "ev2", "ev2_3", "ev2_4", "v1_5", "v2_0",
|
|
// Pebble Time Series
|
|
"snowy_evt2", "snowy_dvt", "spalding_dvt", "snowy_s3", "spalding",
|
|
// Pebble 2 Series
|
|
"silk_evt", "robert_evt", "silk"
|
|
};
|
|
|
|
private static final Random mRandom = new Random();
|
|
|
|
int mFwMajor = 3;
|
|
boolean mEnablePebbleKit = false;
|
|
boolean mAlwaysACKPebbleKit = false;
|
|
private boolean mForceProtocol = false;
|
|
private GBDeviceEventScreenshot mDevEventScreenshot = null;
|
|
private int mScreenshotRemaining = -1;
|
|
|
|
//monochrome black + white
|
|
private static final byte[] clut_pebble = {
|
|
0x00, 0x00, 0x00, 0x00,
|
|
(byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00
|
|
};
|
|
|
|
// linear BGR222 (6 bit, 64 entries)
|
|
private static final byte[] clut_pebbletime = new byte[]{
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x55, 0x00, 0x00, 0x00,
|
|
(byte) 0xaa, 0x00, 0x00, 0x00,
|
|
(byte) 0xff, 0x00, 0x00, 0x00,
|
|
|
|
0x00, 0x55, 0x00, 0x00,
|
|
0x55, 0x55, 0x00, 0x00,
|
|
(byte) 0xaa, 0x55, 0x00, 0x00,
|
|
(byte) 0xff, 0x55, 0x00, 0x00,
|
|
|
|
0x00, (byte) 0xaa, 0x00, 0x00,
|
|
0x55, (byte) 0xaa, 0x00, 0x00,
|
|
(byte) 0xaa, (byte) 0xaa, 0x00, 0x00,
|
|
(byte) 0xff, (byte) 0xaa, 0x00, 0x00,
|
|
|
|
0x00, (byte) 0xff, 0x00, 0x00,
|
|
0x55, (byte) 0xff, 0x00, 0x00,
|
|
(byte) 0xaa, (byte) 0xff, 0x00, 0x00,
|
|
(byte) 0xff, (byte) 0xff, 0x00, 0x00,
|
|
|
|
0x00, 0x00, 0x55, 0x00,
|
|
0x55, 0x00, 0x55, 0x00,
|
|
(byte) 0xaa, 0x00, 0x55, 0x00,
|
|
(byte) 0xff, 0x00, 0x55, 0x00,
|
|
|
|
0x00, 0x55, 0x55, 0x00,
|
|
0x55, 0x55, 0x55, 0x00,
|
|
(byte) 0xaa, 0x55, 0x55, 0x00,
|
|
(byte) 0xff, 0x55, 0x55, 0x00,
|
|
|
|
0x00, (byte) 0xaa, 0x55, 0x00,
|
|
0x55, (byte) 0xaa, 0x55, 0x00,
|
|
(byte) 0xaa, (byte) 0xaa, 0x55, 0x00,
|
|
(byte) 0xff, (byte) 0xaa, 0x55, 0x00,
|
|
|
|
0x00, (byte) 0xff, 0x55, 0x00,
|
|
0x55, (byte) 0xff, 0x55, 0x00,
|
|
(byte) 0xaa, (byte) 0xff, 0x55, 0x00,
|
|
(byte) 0xff, (byte) 0xff, 0x55, 0x00,
|
|
|
|
0x00, 0x00, (byte) 0xaa, 0x00,
|
|
0x55, 0x00, (byte) 0xaa, 0x00,
|
|
(byte) 0xaa, 0x00, (byte) 0xaa, 0x00,
|
|
(byte) 0xff, 0x00, (byte) 0xaa, 0x00,
|
|
|
|
0x00, 0x55, (byte) 0xaa, 0x00,
|
|
0x55, 0x55, (byte) 0xaa, 0x00,
|
|
(byte) 0xaa, 0x55, (byte) 0xaa, 0x00,
|
|
(byte) 0xff, 0x55, (byte) 0xaa, 0x00,
|
|
|
|
0x00, (byte) 0xaa, (byte) 0xaa, 0x00,
|
|
0x55, (byte) 0xaa, (byte) 0xaa, 0x00,
|
|
(byte) 0xaa, (byte) 0xaa, (byte) 0xaa, 0x00,
|
|
(byte) 0xff, (byte) 0xaa, (byte) 0xaa, 0x00,
|
|
|
|
0x00, (byte) 0xff, (byte) 0xaa, 0x00,
|
|
0x55, (byte) 0xff, (byte) 0xaa, 0x00,
|
|
(byte) 0xaa, (byte) 0xff, (byte) 0xaa, 0x00,
|
|
(byte) 0xff, (byte) 0xff, (byte) 0xaa, 0x00,
|
|
|
|
0x00, 0x00, (byte) 0xff, 0x00,
|
|
0x55, 0x00, (byte) 0xff, 0x00,
|
|
(byte) 0xaa, 0x00, (byte) 0xff, 0x00,
|
|
(byte) 0xff, 0x00, (byte) 0xff, 0x00,
|
|
|
|
0x00, 0x55, (byte) 0xff, 0x00,
|
|
0x55, 0x55, (byte) 0xff, 0x00,
|
|
(byte) 0xaa, 0x55, (byte) 0xff, 0x00,
|
|
(byte) 0xff, 0x55, (byte) 0xff, 0x00,
|
|
|
|
0x00, (byte) 0xaa, (byte) 0xff, 0x00,
|
|
0x55, (byte) 0xaa, (byte) 0xff, 0x00,
|
|
(byte) 0xaa, (byte) 0xaa, (byte) 0xff, 0x00,
|
|
(byte) 0xff, (byte) 0xaa, (byte) 0xff, 0x00,
|
|
|
|
0x00, (byte) 0xff, (byte) 0xff, 0x00,
|
|
0x55, (byte) 0xff, (byte) 0xff, 0x00,
|
|
(byte) 0xaa, (byte) 0xff, (byte) 0xff, 0x00,
|
|
(byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00,
|
|
|
|
};
|
|
|
|
|
|
byte last_id = -1;
|
|
private final ArrayList<UUID> tmpUUIDS = new ArrayList<>();
|
|
|
|
public static final UUID UUID_PEBBLE_HEALTH = UUID.fromString("36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c"); // FIXME: store somewhere else, this is also accessed by other code
|
|
public static final UUID UUID_WORKOUT = UUID.fromString("fef82c82-7176-4e22-88de-35a3fc18d43f"); // FIXME: store somewhere else, this is also accessed by other code
|
|
public static final UUID UUID_WEATHER = UUID.fromString("61b22bc8-1e29-460d-a236-3fe409a439ff"); // FIXME: store somewhere else, this is also accessed by other code
|
|
public static final UUID UUID_NOTIFICATIONS = UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1");
|
|
|
|
private static final UUID UUID_GBPEBBLE = UUID.fromString("61476764-7465-7262-6469-656775527a6c");
|
|
private static final UUID UUID_MORPHEUZ = UUID.fromString("5be44f1d-d262-4ea6-aa30-ddbec1e3cab2");
|
|
private static final UUID UUID_MISFIT = UUID.fromString("0b73b76a-cd65-4dc2-9585-aaa213320858");
|
|
private static final UUID UUID_PEBBLE_TIMESTYLE = UUID.fromString("4368ffa4-f0fb-4823-90be-f754b076bdaa");
|
|
private static final UUID UUID_PEBSTYLE = UUID.fromString("da05e84d-e2a2-4020-a2dc-9cdcf265fcdd");
|
|
private static final UUID UUID_MARIOTIME = UUID.fromString("43caa750-2896-4f46-94dc-1adbd4bc1ff3");
|
|
private static final UUID UUID_HELTHIFY = UUID.fromString("7ee97b2c-95e8-4720-b94e-70fccd905d98");
|
|
private static final UUID UUID_TREKVOLLE = UUID.fromString("2da02267-7a19-4e49-9ed1-439d25db14e4");
|
|
private static final UUID UUID_SQUARE = UUID.fromString("cb332373-4ee5-4c5c-8912-4f62af2d756c");
|
|
private static final UUID UUID_ZALEWSZCZAK_CROWEX = UUID.fromString("a88b3151-2426-43c6-b1d0-9b288b3ec47e");
|
|
private static final UUID UUID_ZALEWSZCZAK_FANCY = UUID.fromString("014e17bf-5878-4781-8be1-8ef998cee1ba");
|
|
private static final UUID UUID_ZALEWSZCZAK_TALLY = UUID.fromString("abb51965-52e2-440a-b93c-843eeacb697d");
|
|
private static final UUID UUID_OBSIDIAN = UUID.fromString("ef42caba-0c65-4879-ab23-edd2bde68824");
|
|
private static final UUID UUID_SIMPLY_LIGHT = UUID.fromString("04a6e68a-42d6-4738-87b2-1c80a994dee4");
|
|
private static final UUID UUID_M7S = UUID.fromString("03adc57a-569b-4669-9a80-b505eaea314d");
|
|
private static final UUID UUID_YWEATHER = UUID.fromString("35a28a4d-0c9f-408f-9c6d-551e65f03186");
|
|
private static final UUID UUID_REALWEATHER = UUID.fromString("1f0b0701-cc8f-47ec-86e7-7181397f9a52");
|
|
|
|
private static final UUID UUID_ZERO = new UUID(0, 0);
|
|
|
|
private static final UUID UUID_LOCATION = UUID.fromString("2c7e6a86-51e5-4ddd-b606-db43d1e4ad28"); // might be the location of "Berlin" or "Auto"
|
|
|
|
private final Map<UUID, AppMessageHandler> mAppMessageHandlers = new HashMap<>();
|
|
|
|
private UUID currentRunningApp = UUID_ZERO;
|
|
|
|
public PebbleProtocol(GBDevice device) {
|
|
super(device);
|
|
mAppMessageHandlers.put(UUID_MORPHEUZ, new AppMessageHandlerMorpheuz(UUID_MORPHEUZ, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_MISFIT, new AppMessageHandlerMisfit(UUID_MISFIT, PebbleProtocol.this));
|
|
if (!GBApplication.getGBPrefs().isBackgroundJsEnabled()) {
|
|
mAppMessageHandlers.put(UUID_PEBBLE_TIMESTYLE, new AppMessageHandlerTimeStylePebble(UUID_PEBBLE_TIMESTYLE, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_PEBSTYLE, new AppMessageHandlerPebStyle(UUID_PEBSTYLE, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_MARIOTIME, new AppMessageHandlerMarioTime(UUID_MARIOTIME, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_HELTHIFY, new AppMessageHandlerHealthify(UUID_HELTHIFY, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_TREKVOLLE, new AppMessageHandlerTrekVolle(UUID_TREKVOLLE, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_SQUARE, new AppMessageHandlerSquare(UUID_SQUARE, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_CROWEX, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_CROWEX, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_FANCY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_FANCY, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_TALLY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_TALLY, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_OBSIDIAN, new AppMessageHandlerObsidian(UUID_OBSIDIAN, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_GBPEBBLE, new AppMessageHandlerGBPebble(UUID_GBPEBBLE, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_SIMPLY_LIGHT, new AppMessageHandlerSimplyLight(UUID_SIMPLY_LIGHT, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_M7S, new AppMessageHandlerM7S(UUID_M7S, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_YWEATHER, new AppMessageHandlerRealWeather(UUID_YWEATHER, PebbleProtocol.this));
|
|
mAppMessageHandlers.put(UUID_REALWEATHER, new AppMessageHandlerRealWeather(UUID_REALWEATHER, PebbleProtocol.this));
|
|
}
|
|
}
|
|
|
|
private final HashMap<Byte, DatalogSession> mDatalogSessions = new HashMap<>();
|
|
|
|
private Integer[] idLookup = new Integer[256];
|
|
|
|
private byte[] encodeSimpleMessage(short endpoint, byte command) {
|
|
final short LENGTH_SIMPLEMESSAGE = 1;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SIMPLEMESSAGE);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_SIMPLEMESSAGE);
|
|
buf.putShort(endpoint);
|
|
buf.put(command);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeMessage(short endpoint, byte type, int cookie, String[] parts) {
|
|
// Calculate length first
|
|
int length = LENGTH_PREFIX + 1;
|
|
if (parts != null) {
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
length++; // encode null or empty strings as 0x00 later
|
|
continue;
|
|
}
|
|
length += (1 + s.getBytes().length);
|
|
}
|
|
}
|
|
if (endpoint == ENDPOINT_PHONECONTROL) {
|
|
length += 4; //for cookie;
|
|
}
|
|
|
|
// Encode Prefix
|
|
ByteBuffer buf = ByteBuffer.allocate(length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) (length - LENGTH_PREFIX));
|
|
buf.putShort(endpoint);
|
|
buf.put(type);
|
|
|
|
if (endpoint == ENDPOINT_PHONECONTROL) {
|
|
buf.putInt(cookie);
|
|
}
|
|
// Encode Pascal-Style Strings
|
|
if (parts != null) {
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
buf.put((byte) 0x00);
|
|
continue;
|
|
}
|
|
|
|
int partlength = s.getBytes().length;
|
|
if (partlength > 255) partlength = 255;
|
|
buf.put((byte) partlength);
|
|
buf.put(s.getBytes(), 0, partlength);
|
|
}
|
|
}
|
|
return buf.array();
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeNotification(NotificationSpec notificationSpec) {
|
|
int id = notificationSpec.getId() != -1 ? notificationSpec.getId() : mRandom.nextInt();
|
|
String title;
|
|
String subtitle = null;
|
|
|
|
// for SMS that came in though the SMS receiver
|
|
if (notificationSpec.sender != null) {
|
|
title = notificationSpec.sender;
|
|
subtitle = notificationSpec.subject;
|
|
} else {
|
|
title = notificationSpec.title;
|
|
}
|
|
|
|
long ts = System.currentTimeMillis();
|
|
if (mFwMajor < 3) {
|
|
ts += (SimpleTimeZone.getDefault().getOffset(ts));
|
|
}
|
|
ts /= 1000;
|
|
|
|
if (mFwMajor >= 3 || mForceProtocol || notificationSpec.type != NotificationType.GENERIC_EMAIL) {
|
|
// 3.x notification
|
|
return encodeNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body,
|
|
notificationSpec.type, notificationSpec.pebbleColor,
|
|
notificationSpec.cannedReplies, notificationSpec.attachedActions);
|
|
} else {
|
|
// 1.x notification on FW 2.X
|
|
String[] parts = {title, notificationSpec.body, String.valueOf(ts), subtitle};
|
|
// be aware that type is at this point always NOTIFICATION_EMAIL
|
|
return encodeMessage(ENDPOINT_NOTIFICATION, NOTIFICATION_EMAIL, 0, parts);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeDeleteNotification(int id) {
|
|
return encodeBlobdb(new UUID(GB_UUID_MASK, id), BLOBDB_DELETE, BLOBDB_NOTIFICATION, null);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
|
long id = calendarEventSpec.id != -1 ? calendarEventSpec.id : mRandom.nextLong();
|
|
int iconId;
|
|
ArrayList<Pair<Integer, Object>> attributes = new ArrayList<>();
|
|
attributes.add(new Pair<>(1, (Object) calendarEventSpec.title));
|
|
switch (calendarEventSpec.type) {
|
|
case CalendarEventSpec.TYPE_SUNRISE:
|
|
iconId = PebbleIconID.SUNRISE;
|
|
break;
|
|
case CalendarEventSpec.TYPE_SUNSET:
|
|
iconId = PebbleIconID.SUNSET;
|
|
break;
|
|
default:
|
|
iconId = PebbleIconID.TIMELINE_CALENDAR;
|
|
attributes.add(new Pair<>(3, (Object) calendarEventSpec.description));
|
|
attributes.add(new Pair<>(11, (Object) calendarEventSpec.location));
|
|
}
|
|
|
|
return encodeTimelinePin(new UUID(GB_UUID_MASK | calendarEventSpec.type, id), calendarEventSpec.timestamp, (short) (calendarEventSpec.durationInSeconds / 60), iconId, attributes);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeDeleteCalendarEvent(byte type, long id) {
|
|
return encodeBlobdb(new UUID(GB_UUID_MASK | type, id), BLOBDB_DELETE, BLOBDB_PIN, null);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeSetTime() {
|
|
final short LENGTH_SETTIME = 5;
|
|
long ts = System.currentTimeMillis();
|
|
long ts_offset = (SimpleTimeZone.getDefault().getOffset(ts));
|
|
ByteBuffer buf;
|
|
if (mFwMajor >= 3) {
|
|
String timezone = SimpleTimeZone.getDefault().getID();
|
|
short length = (short) (LENGTH_SETTIME + timezone.getBytes().length + 3);
|
|
buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(length);
|
|
buf.putShort(ENDPOINT_TIME);
|
|
buf.put(TIME_SETTIME_UTC);
|
|
buf.putInt((int) (ts / 1000));
|
|
buf.putShort((short) (ts_offset / 60000));
|
|
buf.put((byte) timezone.getBytes().length);
|
|
buf.put(timezone.getBytes());
|
|
LOG.info(timezone);
|
|
} else {
|
|
buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SETTIME);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_SETTIME);
|
|
buf.putShort(ENDPOINT_TIME);
|
|
buf.put(TIME_SETTIME);
|
|
buf.putInt((int) ((ts + ts_offset) / 1000));
|
|
}
|
|
return buf.array();
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeFindDevice(boolean start) {
|
|
return encodeSetCallState("Where are you?", "Gadgetbridge", start ? CallSpec.CALL_INCOMING : CallSpec.CALL_END);
|
|
/*
|
|
int ts = (int) (System.currentTimeMillis() / 1000);
|
|
|
|
if (start) {
|
|
//return encodeWeatherPin(ts, "Weather", "1°/-1°", "Gadgetbridge is Sunny", "Berlin", 37);
|
|
}
|
|
*/
|
|
}
|
|
|
|
private byte[] encodeBlobdb(Object key, byte command, byte db, byte[] blob) {
|
|
|
|
int length = 5;
|
|
|
|
int key_length;
|
|
if (key instanceof UUID) {
|
|
key_length = LENGTH_UUID;
|
|
} else if (key instanceof String) {
|
|
key_length = ((String) key).getBytes().length;
|
|
} else {
|
|
LOG.warn("unknown key type");
|
|
return null;
|
|
}
|
|
if (key_length > 255) {
|
|
LOG.warn("key is too long");
|
|
return null;
|
|
}
|
|
length += key_length;
|
|
|
|
if (blob != null) {
|
|
length += blob.length + 2;
|
|
}
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) length);
|
|
buf.putShort(ENDPOINT_BLOBDB);
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put(command);
|
|
buf.putShort((short) mRandom.nextInt()); // token
|
|
buf.put(db);
|
|
|
|
buf.put((byte) key_length);
|
|
if (key instanceof UUID) {
|
|
UUID uuid = (UUID) key;
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
} else {
|
|
buf.put(((String) key).getBytes());
|
|
}
|
|
|
|
if (blob != null) {
|
|
buf.putShort((short) blob.length);
|
|
buf.put(blob);
|
|
}
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeActivateHealth(boolean activate) {
|
|
byte[] blob;
|
|
if (activate) {
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(9);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
ActivityUser activityUser = new ActivityUser();
|
|
int heightMm = activityUser.getHeightCm() * 10;
|
|
buf.putShort((short) heightMm);
|
|
int weigthDag = activityUser.getWeightKg() * 100;
|
|
buf.putShort((short) weigthDag);
|
|
buf.put((byte) 0x01); //activate tracking
|
|
buf.put((byte) 0x00); //activity Insights
|
|
buf.put((byte) 0x00); //sleep Insights
|
|
buf.put((byte) activityUser.getAge());
|
|
buf.put((byte) activityUser.getGender());
|
|
blob = buf.array();
|
|
} else {
|
|
blob = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
|
}
|
|
return encodeBlobdb("activityPreferences", BLOBDB_INSERT, BLOBDB_PREFERENCES, blob);
|
|
}
|
|
|
|
byte[] encodeSetSaneDistanceUnit(boolean sane) {
|
|
byte value;
|
|
if (sane) {
|
|
value = 0x00;
|
|
} else {
|
|
value = 0x01;
|
|
}
|
|
return encodeBlobdb("unitsDistance", BLOBDB_INSERT, BLOBDB_PREFERENCES, new byte[]{value});
|
|
}
|
|
|
|
|
|
byte[] encodeActivateHRM(boolean activate) {
|
|
return encodeBlobdb("hrmPreferences", BLOBDB_INSERT, BLOBDB_PREFERENCES,
|
|
activate ? new byte[]{0x01} : new byte[]{0x00});
|
|
}
|
|
|
|
byte[] encodeActivateWeather(boolean activate) {
|
|
if (activate) {
|
|
ByteBuffer buf = ByteBuffer.allocate(0x61);
|
|
buf.put((byte) 1);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putLong(UUID_LOCATION.getMostSignificantBits());
|
|
buf.putLong(UUID_LOCATION.getLeastSignificantBits());
|
|
// disable remaining 5 possible location
|
|
buf.put(new byte[60 - LENGTH_UUID]);
|
|
return encodeBlobdb("weatherApp", BLOBDB_INSERT, BLOBDB_APPSETTINGS, buf.array());
|
|
} else {
|
|
return encodeBlobdb("weatherApp", BLOBDB_DELETE, BLOBDB_APPSETTINGS, null);
|
|
}
|
|
}
|
|
|
|
byte[] encodeReportDataLogSessions() {
|
|
return encodeSimpleMessage(ENDPOINT_DATALOG, DATALOG_REPORTSESSIONS);
|
|
}
|
|
|
|
private byte[] encodeBlobDBClear(byte database) {
|
|
final short LENGTH_BLOBDB_CLEAR = 4;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_BLOBDB_CLEAR);
|
|
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_BLOBDB_CLEAR);
|
|
buf.putShort(ENDPOINT_BLOBDB);
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put(BLOBDB_CLEAR);
|
|
buf.putShort((short) mRandom.nextInt()); // token
|
|
buf.put(database);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeTimelinePin(UUID uuid, int timestamp, short duration, int icon_id, List<Pair<Integer, Object>> attributes) {
|
|
final short TIMELINE_PIN_LENGTH = 46;
|
|
|
|
//FIXME: dont depend layout on icon :P
|
|
byte layout_id = 0x01;
|
|
if (icon_id == PebbleIconID.TIMELINE_CALENDAR) {
|
|
layout_id = 0x02;
|
|
}
|
|
icon_id |= 0x80000000;
|
|
byte attributes_count = 1;
|
|
byte actions_count = 0;
|
|
|
|
int attributes_length = 7;
|
|
for (Pair<Integer, Object> pair : attributes) {
|
|
if (pair.first == null || pair.second == null)
|
|
continue;
|
|
attributes_count++;
|
|
if (pair.second instanceof Integer) {
|
|
attributes_length += 7;
|
|
} else if (pair.second instanceof Byte) {
|
|
attributes_length += 4;
|
|
} else if (pair.second instanceof String) {
|
|
attributes_length += ((String) pair.second).getBytes().length + 3;
|
|
} else if (pair.second instanceof byte[]) {
|
|
attributes_length += ((byte[]) pair.second).length + 3;
|
|
} else {
|
|
LOG.warn("unsupported type for timeline attributes: " + pair.second.getClass().toString());
|
|
}
|
|
}
|
|
|
|
int pin_length = TIMELINE_PIN_LENGTH + attributes_length;
|
|
ByteBuffer buf = ByteBuffer.allocate(pin_length);
|
|
|
|
// pin - 46 bytes
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.putLong(0); // parent
|
|
buf.putLong(0);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(timestamp); // 32-bit timestamp
|
|
buf.putShort(duration);
|
|
buf.put((byte) 0x02); // type (0x02 = pin)
|
|
buf.putShort((short) 0x0001); // flags 0x0001 = ?
|
|
buf.put(layout_id); // layout was (0x02 = pin?), 0x01 needed for subtitle but seems to do no harm if there isn't one
|
|
buf.putShort((short) attributes_length); // total length of all attributes and actions in bytes
|
|
buf.put(attributes_count);
|
|
buf.put(actions_count);
|
|
|
|
buf.put((byte) 4); // icon
|
|
buf.putShort((short) 4); // length of int
|
|
buf.putInt(icon_id);
|
|
|
|
for (Pair<Integer, Object> pair : attributes) {
|
|
if (pair.first == null || pair.second == null)
|
|
continue;
|
|
buf.put(pair.first.byteValue());
|
|
if (pair.second instanceof Integer) {
|
|
buf.putShort((short) 4);
|
|
buf.putInt(((Integer) pair.second));
|
|
} else if (pair.second instanceof Byte) {
|
|
buf.putShort((short) 1);
|
|
buf.put((Byte) pair.second);
|
|
} else if (pair.second instanceof String) {
|
|
buf.putShort((short) ((String) pair.second).getBytes().length);
|
|
buf.put(((String) pair.second).getBytes());
|
|
} else if (pair.second instanceof byte[]) {
|
|
buf.putShort((short) ((byte[]) pair.second).length);
|
|
buf.put((byte[]) pair.second);
|
|
}
|
|
}
|
|
|
|
return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array());
|
|
}
|
|
|
|
private byte[] encodeNotification(int id, int timestamp, String title, String subtitle, String body,
|
|
NotificationType notificationType, byte backgroundColor, String[] cannedReplies, ArrayList<Action> attachedActions) {
|
|
final short NOTIFICATION_PIN_LENGTH = 46;
|
|
final short ACTION_LENGTH_MIN = 6;
|
|
|
|
String[] parts = {title, subtitle, body};
|
|
|
|
if(notificationType == null) {
|
|
notificationType = NotificationType.UNKNOWN;
|
|
}
|
|
|
|
int icon_id = notificationType.icon;
|
|
|
|
// Calculate length first
|
|
int actions_count = 0;
|
|
short actions_length = 0;
|
|
|
|
int replies_length = 0;
|
|
if (cannedReplies != null && cannedReplies.length > 0) {
|
|
//do not increment actions_count! reply is an action and was already added above
|
|
for (String reply : cannedReplies) {
|
|
replies_length += reply.getBytes().length + 1;
|
|
}
|
|
replies_length--;
|
|
//similarly, only the replies length has to be added, the length for the bare action was already added above
|
|
|
|
}
|
|
|
|
if (attachedActions != null && attachedActions.size() > 0) {
|
|
for (Action act : attachedActions) {
|
|
actions_count++;
|
|
actions_length += (short) (ACTION_LENGTH_MIN + act.title.getBytes().length);
|
|
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
|
actions_length += (short) replies_length + 3; // 3 = attribute id (byte) + length(short)
|
|
}
|
|
}
|
|
}
|
|
|
|
byte attributes_count = 0;
|
|
short attributes_length = actions_length;
|
|
if (mFwMajor >= 3) {
|
|
attributes_count += 2; // icon
|
|
attributes_length += 11;
|
|
}
|
|
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
attributes_count++;
|
|
attributes_length += (short) (3 + s.getBytes().length);
|
|
}
|
|
|
|
short length;
|
|
int max_partlength;
|
|
byte dismiss_action_type;
|
|
ByteBuffer buf;
|
|
if (mFwMajor >= 3) {
|
|
length = (short) (NOTIFICATION_PIN_LENGTH + attributes_length);
|
|
max_partlength = 512;
|
|
dismiss_action_type = 0x02; // generic action, dismiss did not do anything
|
|
buf = ByteBuffer.allocate(length);
|
|
} else {
|
|
length = (short) (21 + attributes_length);
|
|
max_partlength = 256;
|
|
dismiss_action_type = 0x04; // dismiss
|
|
buf = ByteBuffer.allocate(length + LENGTH_PREFIX);
|
|
}
|
|
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
|
|
if (mFwMajor >= 3) {
|
|
// pin - 46 bytes
|
|
buf.putLong(GB_UUID_MASK);
|
|
buf.putLong(id);
|
|
buf.putLong(UUID_NOTIFICATIONS.getMostSignificantBits());
|
|
buf.putLong(UUID_NOTIFICATIONS.getLeastSignificantBits());
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(timestamp); // 32-bit timestamp
|
|
buf.putShort((short) 0); // duration
|
|
buf.put((byte) 0x01); // type (0x01 = notification)
|
|
buf.putShort((short) 0x0001); // flags 0x0001 = ?
|
|
buf.put((byte) 0x04); // layout (0x04 = notification?)
|
|
buf.putShort(attributes_length); // total length of all attributes and actions in bytes
|
|
} else {
|
|
buf.putShort(length);
|
|
buf.putShort(ENDPOINT_EXTENSIBLENOTIFS);
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN); // !
|
|
|
|
buf.put((byte) 0x00); // ?
|
|
buf.put((byte) 0x01); // add notifications
|
|
buf.putInt(0x00000000); // flags - ?
|
|
buf.putInt(id);
|
|
buf.putInt(0x00000000); // ANCS id
|
|
buf.putInt(timestamp);
|
|
buf.put((byte) 0x01); // layout - ?
|
|
|
|
}
|
|
buf.put(attributes_count);
|
|
buf.put((byte) actions_count);
|
|
|
|
byte attribute_id = 0;
|
|
// Encode Pascal-Style Strings
|
|
for (String s : parts) {
|
|
attribute_id++;
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
|
|
int partlength = s.getBytes().length;
|
|
if (partlength > max_partlength) partlength = max_partlength;
|
|
buf.put(attribute_id);
|
|
buf.putShort((short) partlength);
|
|
buf.put(s.getBytes(), 0, partlength);
|
|
}
|
|
|
|
if (mFwMajor >= 3) {
|
|
buf.put((byte) 4); // icon
|
|
buf.putShort((short) 4); // length of int
|
|
buf.putInt(0x80000000 | icon_id);
|
|
|
|
buf.put((byte) 28); // background_color
|
|
buf.putShort((short) 1); // length of int
|
|
buf.put(backgroundColor);
|
|
}
|
|
|
|
if (attachedActions != null && attachedActions.size() > 0) {
|
|
for (int ai = 0 ; ai<attachedActions.size(); ai++) {
|
|
Action act = attachedActions.get(ai);
|
|
switch (act.type) {
|
|
case Action.TYPE_SYNTECTIC_OPEN:
|
|
buf.put((byte) 0x01);
|
|
break;
|
|
case Action.TYPE_SYNTECTIC_DISMISS:
|
|
buf.put((byte) 0x02);
|
|
break;
|
|
case Action.TYPE_SYNTECTIC_DISMISS_ALL:
|
|
buf.put((byte) 0x03);
|
|
break;
|
|
case Action.TYPE_SYNTECTIC_MUTE:
|
|
buf.put((byte) 0x04);
|
|
break;
|
|
default:
|
|
buf.put((byte) (0x05 + ai));
|
|
}
|
|
|
|
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
|
buf.put((byte) 0x03); // reply action
|
|
buf.put((byte) 0x02); // number attributes
|
|
} else {
|
|
if (act.type == Action.TYPE_SYNTECTIC_DISMISS) {
|
|
buf.put(dismiss_action_type);
|
|
} else {
|
|
buf.put((byte) 0x02); // generic action
|
|
}
|
|
buf.put((byte) 0x01); // number attributes
|
|
}
|
|
|
|
buf.put((byte) 0x01); // attribute id (title)
|
|
buf.putShort((short) act.title.getBytes().length);
|
|
buf.put(act.title.getBytes());
|
|
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
|
buf.put((byte) 0x08); // canned replies
|
|
buf.putShort((short) replies_length);
|
|
if (cannedReplies != null && cannedReplies.length > 0) {
|
|
for (int i = 0; i < cannedReplies.length - 1; i++) {
|
|
buf.put(cannedReplies[i].getBytes());
|
|
buf.put((byte) 0x00);
|
|
}
|
|
// last one must not be zero terminated, else we get an additional emply reply
|
|
buf.put(cannedReplies[cannedReplies.length - 1].getBytes());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (mFwMajor >= 3) {
|
|
return encodeBlobdb(UUID.randomUUID(), BLOBDB_INSERT, BLOBDB_NOTIFICATION, buf.array());
|
|
} else {
|
|
return buf.array();
|
|
}
|
|
}
|
|
|
|
private byte[] encodeActionResponse2x(int id, byte actionId, int iconId, String caption) {
|
|
short length = (short) (18 + caption.getBytes().length);
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(length);
|
|
buf.putShort(ENDPOINT_EXTENSIBLENOTIFS);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put(NOTIFICATIONACTION_RESPONSE);
|
|
buf.putInt(id);
|
|
buf.put(actionId);
|
|
buf.put(NOTIFICATIONACTION_ACK);
|
|
buf.put((byte) 2); //nr of attributes
|
|
buf.put((byte) 6); // icon
|
|
buf.putShort((short) 4); // length
|
|
buf.putInt(iconId);
|
|
buf.put((byte) 2); // title
|
|
buf.putShort((short) caption.getBytes().length);
|
|
buf.put(caption.getBytes());
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeWeatherPin(int timestamp, String title, String subtitle, String body, String location, int iconId) {
|
|
final short NOTIFICATION_PIN_LENGTH = 46;
|
|
final short ACTION_LENGTH_MIN = 6;
|
|
|
|
String[] parts = {title, subtitle, body, location, "test", "test"};
|
|
|
|
// Calculate length first
|
|
byte actions_count = 1;
|
|
short actions_length;
|
|
String remove_string = "Remove";
|
|
actions_length = (short) (ACTION_LENGTH_MIN * actions_count + remove_string.getBytes().length);
|
|
|
|
byte attributes_count = 3;
|
|
short attributes_length = (short) (21 + actions_length);
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
attributes_count++;
|
|
attributes_length += (short) (3 + s.getBytes().length);
|
|
}
|
|
|
|
UUID uuid = UUID.fromString("61b22bc8-1e29-460d-a236-3fe409a43901");
|
|
|
|
short pin_length = (short) (NOTIFICATION_PIN_LENGTH + attributes_length);
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(pin_length);
|
|
|
|
// pin (46 bytes)
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits() | 0xff);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(timestamp); // 32-bit timestamp
|
|
buf.putShort((short) 0); // duration
|
|
buf.put((byte) 0x02); // type (0x02 = pin)
|
|
buf.putShort((short) 0x0001); // flags 0x0001 = ?
|
|
buf.put((byte) 0x06); // layout (0x06 = weather)
|
|
buf.putShort(attributes_length); // total length of all attributes and actions in bytes
|
|
buf.put(attributes_count);
|
|
buf.put(actions_count);
|
|
|
|
byte attribute_id = 0;
|
|
// Encode Pascal-Style Strings
|
|
for (String s : parts) {
|
|
attribute_id++;
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
|
|
int partlength = s.getBytes().length;
|
|
if (partlength > 512) partlength = 512;
|
|
if (attribute_id == 4) {
|
|
buf.put((byte) 11);
|
|
} else if (attribute_id == 5) {
|
|
buf.put((byte) 25);
|
|
} else if (attribute_id == 6) {
|
|
buf.put((byte) 26);
|
|
} else {
|
|
buf.put(attribute_id);
|
|
}
|
|
buf.putShort((short) partlength);
|
|
buf.put(s.getBytes(), 0, partlength);
|
|
}
|
|
|
|
buf.put((byte) 4); // icon
|
|
buf.putShort((short) 4); // length of int
|
|
buf.putInt(0x80000000 | iconId);
|
|
|
|
buf.put((byte) 6); // icon
|
|
buf.putShort((short) 4); // length of int
|
|
buf.putInt(0x80000000 | iconId);
|
|
|
|
buf.put((byte) 14); // last updated
|
|
buf.putShort((short) 4); // length of int
|
|
buf.putInt(timestamp);
|
|
|
|
// remove action
|
|
buf.put((byte) 123); // action id
|
|
buf.put((byte) 0x09); // remove
|
|
buf.put((byte) 0x01); // number attributes
|
|
buf.put((byte) 0x01); // attribute id (title)
|
|
buf.putShort((short) remove_string.getBytes().length);
|
|
buf.put(remove_string.getBytes());
|
|
|
|
return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array());
|
|
}
|
|
|
|
|
|
@Override
|
|
public byte[] encodeSendWeather(WeatherSpec weatherSpec) {
|
|
byte[] forecastProtocol = null;
|
|
byte[] watchfaceProtocol = null;
|
|
int length = 0;
|
|
if (mFwMajor >= 4) {
|
|
forecastProtocol = encodeWeatherForecast(weatherSpec);
|
|
length += forecastProtocol.length;
|
|
}
|
|
AppMessageHandler handler = mAppMessageHandlers.get(currentRunningApp);
|
|
if (handler != null) {
|
|
watchfaceProtocol = handler.encodeUpdateWeather(weatherSpec);
|
|
if (watchfaceProtocol != null) {
|
|
length += watchfaceProtocol.length;
|
|
}
|
|
}
|
|
ByteBuffer buf = ByteBuffer.allocate(length);
|
|
|
|
if (forecastProtocol != null) {
|
|
buf.put(forecastProtocol);
|
|
}
|
|
if (watchfaceProtocol != null) {
|
|
buf.put(watchfaceProtocol);
|
|
}
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeWeatherForecast(WeatherSpec weatherSpec) {
|
|
|
|
short currentTemp = (short) (weatherSpec.currentTemp - 273);
|
|
short todayMax = (short) (weatherSpec.todayMaxTemp - 273);
|
|
short todayMin = (short) (weatherSpec.todayMinTemp - 273);
|
|
short tomorrowMax = 0;
|
|
short tomorrowMin = 0;
|
|
int tomorrowConditionCode = 0;
|
|
if (weatherSpec.forecasts.size() > 0) {
|
|
WeatherSpec.Forecast tomorrow = weatherSpec.forecasts.get(0);
|
|
tomorrowMax = (short) (tomorrow.maxTemp - 273);
|
|
tomorrowMin = (short) (tomorrow.minTemp - 273);
|
|
tomorrowConditionCode = tomorrow.conditionCode;
|
|
}
|
|
|
|
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
|
if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
|
|
currentTemp = (short) (currentTemp * 1.8f + 32);
|
|
todayMax = (short) (todayMax * 1.8f + 32);
|
|
todayMin = (short) (todayMin * 1.8f + 32);
|
|
tomorrowMax = (short) (tomorrowMax * 1.8f + 32);
|
|
tomorrowMin = (short) (tomorrowMin * 1.8f + 32);
|
|
}
|
|
final short WEATHER_FORECAST_LENGTH = 20;
|
|
|
|
String[] parts = {weatherSpec.location, weatherSpec.currentCondition};
|
|
|
|
// Calculate length first
|
|
short attributes_length = 0;
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
attributes_length += (short) (2 + s.getBytes().length);
|
|
}
|
|
|
|
short pin_length = (short) (WEATHER_FORECAST_LENGTH + attributes_length);
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(pin_length);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put((byte) 3); // unknown, always 3?
|
|
buf.putShort(currentTemp);
|
|
buf.put(Weather.mapToPebbleCondition(weatherSpec.currentConditionCode));
|
|
buf.putShort(todayMax);
|
|
buf.putShort(todayMin);
|
|
buf.put(Weather.mapToPebbleCondition(tomorrowConditionCode));
|
|
buf.putShort(tomorrowMax);
|
|
buf.putShort(tomorrowMin);
|
|
buf.putInt(weatherSpec.timestamp);
|
|
buf.put((byte) 0); // automatic location 0=manual 1=auto
|
|
buf.putShort(attributes_length);
|
|
|
|
// Encode Pascal-Style Strings
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
continue;
|
|
}
|
|
|
|
int partlength = s.getBytes().length;
|
|
if (partlength > 512) partlength = 512;
|
|
buf.putShort((short) partlength);
|
|
buf.put(s.getBytes(), 0, partlength);
|
|
}
|
|
|
|
return encodeBlobdb(UUID_LOCATION, BLOBDB_INSERT, BLOBDB_WEATHER, buf.array());
|
|
}
|
|
|
|
private byte[] encodeActionResponse(UUID uuid, int iconId, String caption) {
|
|
short length = (short) (29 + caption.getBytes().length);
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(length);
|
|
buf.putShort(ENDPOINT_NOTIFICATIONACTION);
|
|
buf.put(NOTIFICATIONACTION_RESPONSE);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put(NOTIFICATIONACTION_ACK);
|
|
buf.put((byte) 2); //nr of attributes
|
|
buf.put((byte) 6); // icon
|
|
buf.putShort((short) 4); // length
|
|
buf.putInt(0x80000000 | iconId);
|
|
buf.put((byte) 2); // title
|
|
buf.putShort((short) caption.getBytes().length);
|
|
buf.put(caption.getBytes());
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeInstallMetadata(UUID uuid, String appName, short appVersion, short sdkVersion, int flags, int iconId) {
|
|
final short METADATA_LENGTH = 126;
|
|
|
|
byte[] name_buf = new byte[96];
|
|
System.arraycopy(appName.getBytes(), 0, name_buf, 0, appName.getBytes().length);
|
|
ByteBuffer buf = ByteBuffer.allocate(METADATA_LENGTH);
|
|
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putLong(uuid.getMostSignificantBits()); // watchapp uuid
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(flags);
|
|
buf.putInt(iconId);
|
|
buf.putShort(appVersion);
|
|
buf.putShort(sdkVersion);
|
|
buf.put((byte) 0); // app_face_bgcolor
|
|
buf.put((byte) 0); // app_face_template_id
|
|
buf.put(name_buf); // 96 bytes
|
|
|
|
return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_APP, buf.array());
|
|
}
|
|
|
|
byte[] encodeAppFetchAck() {
|
|
final short LENGTH_APPFETCH = 2;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_APPFETCH);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_APPFETCH);
|
|
buf.putShort(ENDPOINT_APPFETCH);
|
|
buf.put((byte) 0x01);
|
|
buf.put((byte) 0x01);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeGetTime() {
|
|
return encodeSimpleMessage(ENDPOINT_TIME, TIME_GETTIME);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeSetCallState(String number, String name, int command) {
|
|
String[] parts = {number, name};
|
|
byte pebbleCmd;
|
|
switch (command) {
|
|
case CallSpec.CALL_START:
|
|
pebbleCmd = PHONECONTROL_START;
|
|
break;
|
|
case CallSpec.CALL_END:
|
|
pebbleCmd = PHONECONTROL_END;
|
|
break;
|
|
case CallSpec.CALL_INCOMING:
|
|
pebbleCmd = PHONECONTROL_INCOMINGCALL;
|
|
break;
|
|
case CallSpec.CALL_OUTGOING:
|
|
// pebbleCmd = PHONECONTROL_OUTGOINGCALL;
|
|
/*
|
|
* HACK/WORKAROUND for non-working outgoing call display.
|
|
* Just send a incoming call command immediately followed by a start call command
|
|
* This prevents vibration of the Pebble.
|
|
*/
|
|
byte[] callmsg = encodeMessage(ENDPOINT_PHONECONTROL, PHONECONTROL_INCOMINGCALL, 0, parts);
|
|
byte[] startmsg = encodeMessage(ENDPOINT_PHONECONTROL, PHONECONTROL_START, 0, parts);
|
|
byte[] msg = new byte[callmsg.length + startmsg.length];
|
|
System.arraycopy(callmsg, 0, msg, 0, callmsg.length);
|
|
System.arraycopy(startmsg, 0, msg, startmsg.length, startmsg.length);
|
|
return msg;
|
|
// END HACK
|
|
default:
|
|
return null;
|
|
}
|
|
return encodeMessage(ENDPOINT_PHONECONTROL, pebbleCmd, 0, parts);
|
|
}
|
|
|
|
public byte[] encodeSetMusicState(byte state, int position, int playRate, byte shuffle, byte repeat) {
|
|
if (mFwMajor < 3) {
|
|
return null;
|
|
}
|
|
|
|
byte playState;
|
|
|
|
switch (state) {
|
|
case MusicStateSpec.STATE_PLAYING:
|
|
playState = MUSICCONTROL_STATE_PLAYING;
|
|
break;
|
|
case MusicStateSpec.STATE_PAUSED:
|
|
playState = MUSICCONTROL_STATE_PAUSED;
|
|
break;
|
|
default:
|
|
playState = MUSICCONTROL_STATE_UNKNOWN;
|
|
break;
|
|
}
|
|
|
|
int length = LENGTH_PREFIX + 12;
|
|
// Encode Prefix
|
|
ByteBuffer buf = ByteBuffer.allocate(length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) (length - LENGTH_PREFIX));
|
|
buf.putShort(ENDPOINT_MUSICCONTROL);
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.put(MUSICCONTROL_SETPLAYSTATE);
|
|
buf.put(playState);
|
|
buf.putInt(position * 1000);
|
|
buf.putInt(playRate);
|
|
buf.put(shuffle);
|
|
buf.put(repeat);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeSetMusicInfo(String artist, String album, String track, int duration, int trackCount, int trackNr) {
|
|
String[] parts = {artist, album, track};
|
|
if (duration == 0 || mFwMajor < 3) {
|
|
return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts);
|
|
} else {
|
|
// Calculate length first
|
|
int length = LENGTH_PREFIX + 9;
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
length++; // encode null or empty strings as 0x00 later
|
|
continue;
|
|
}
|
|
length += (1 + s.getBytes().length);
|
|
}
|
|
|
|
// Encode Prefix
|
|
ByteBuffer buf = ByteBuffer.allocate(length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) (length - LENGTH_PREFIX));
|
|
buf.putShort(ENDPOINT_MUSICCONTROL);
|
|
buf.put(MUSICCONTROL_SETMUSICINFO);
|
|
|
|
// Encode Pascal-Style Strings
|
|
for (String s : parts) {
|
|
if (s == null || s.equals("")) {
|
|
buf.put((byte) 0x00);
|
|
continue;
|
|
}
|
|
|
|
int partlength = s.getBytes().length;
|
|
if (partlength > 255) partlength = 255;
|
|
buf.put((byte) partlength);
|
|
buf.put(s.getBytes(), 0, partlength);
|
|
}
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(duration * 1000);
|
|
buf.putShort((short) (trackCount & 0xffff));
|
|
buf.putShort((short) (trackNr & 0xffff));
|
|
|
|
return buf.array();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeFirmwareVersionReq() {
|
|
return encodeSimpleMessage(ENDPOINT_FIRMWAREVERSION, FIRMWAREVERSION_GETVERSION);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeAppInfoReq() {
|
|
if (mFwMajor >= 3) {
|
|
return null; // can't do this on 3.x :(
|
|
}
|
|
return encodeSimpleMessage(ENDPOINT_APPMANAGER, APPMANAGER_GETUUIDS);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeAppStart(UUID uuid, boolean start) {
|
|
if (mFwMajor >= 3) {
|
|
final short LENGTH_APPRUNSTATE = 17;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_APPRUNSTATE);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_APPRUNSTATE);
|
|
buf.putShort(ENDPOINT_APPRUNSTATE);
|
|
buf.put(start ? APPRUNSTATE_START : APPRUNSTATE_STOP);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
return buf.array();
|
|
} else {
|
|
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
|
|
int param = start ? 1 : 0;
|
|
pairs.add(new Pair<>(1, (Object) param));
|
|
return encodeApplicationMessagePush(ENDPOINT_LAUNCHER, uuid, pairs, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeAppDelete(UUID uuid) {
|
|
if (mFwMajor >= 3) {
|
|
if (UUID_PEBBLE_HEALTH.equals(uuid)) {
|
|
return encodeActivateHealth(false);
|
|
}
|
|
if (UUID_WORKOUT.equals(uuid)) {
|
|
return encodeActivateHRM(false);
|
|
}
|
|
if (UUID_WEATHER.equals(uuid)) { //TODO: probably it wasn't present in firmware 3
|
|
return encodeActivateWeather(false);
|
|
}
|
|
return encodeBlobdb(uuid, BLOBDB_DELETE, BLOBDB_APP, null);
|
|
} else {
|
|
final short LENGTH_REMOVEAPP_2X = 17;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_REMOVEAPP_2X);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_REMOVEAPP_2X);
|
|
buf.putShort(ENDPOINT_APPMANAGER);
|
|
buf.put(APPMANAGER_REMOVEAPP);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
return buf.array();
|
|
}
|
|
}
|
|
|
|
private byte[] encodePhoneVersion2x(byte os) {
|
|
final short LENGTH_PHONEVERSION = 17;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_PHONEVERSION);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_PHONEVERSION);
|
|
buf.putShort(ENDPOINT_PHONEVERSION);
|
|
buf.put((byte) 0x01);
|
|
buf.putInt(-1); //0xffffffff
|
|
|
|
if (os == PHONEVERSION_REMOTE_OS_ANDROID) {
|
|
buf.putInt(PHONEVERSION_SESSION_CAPS_GAMMARAY);
|
|
} else {
|
|
buf.putInt(0);
|
|
}
|
|
buf.putInt(PHONEVERSION_REMOTE_CAPS_SMS | PHONEVERSION_REMOTE_CAPS_TELEPHONY | os);
|
|
|
|
buf.put(PHONEVERSION_APPVERSION_MAGIC);
|
|
buf.put(PHONEVERSION_APPVERSION_MAJOR);
|
|
buf.put(PHONEVERSION_APPVERSION_MINOR);
|
|
buf.put(PHONEVERSION_APPVERSION_PATCH);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodePhoneVersion3x(byte os) {
|
|
final short LENGTH_PHONEVERSION3X = 25;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_PHONEVERSION3X);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_PHONEVERSION3X);
|
|
buf.putShort(ENDPOINT_PHONEVERSION);
|
|
buf.put((byte) 0x01);
|
|
buf.putInt(-1); //0xffffffff
|
|
buf.putInt(0);
|
|
|
|
buf.putInt(os);
|
|
|
|
buf.put(PHONEVERSION_APPVERSION_MAGIC);
|
|
buf.put((byte) 4); // major
|
|
buf.put((byte) 1); // minor
|
|
buf.put((byte) 1); // patch
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putLong(0x00000000000029af); //flags
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodePhoneVersion(byte os) {
|
|
return encodePhoneVersion3x(os);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeReset(int flags) {
|
|
return encodeSimpleMessage(ENDPOINT_RESET, RESET_REBOOT);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeScreenshotReq() {
|
|
return encodeSimpleMessage(ENDPOINT_SCREENSHOT, SCREENSHOT_TAKE);
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeAppReorder(UUID[] uuids) {
|
|
int length = 2 + uuids.length * LENGTH_UUID;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) length);
|
|
buf.putShort(ENDPOINT_APPREORDER);
|
|
buf.put((byte) 0x01);
|
|
buf.put((byte) uuids.length);
|
|
for (UUID uuid : uuids) {
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
}
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
@Override
|
|
public byte[] encodeSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
|
|
|
if (cannedMessagesSpec.cannedMessages == null || cannedMessagesSpec.cannedMessages.length == 0) {
|
|
return null;
|
|
}
|
|
|
|
String blobDBKey;
|
|
switch (cannedMessagesSpec.type) {
|
|
case CannedMessagesSpec.TYPE_MISSEDCALLS:
|
|
blobDBKey = "com.pebble.android.phone";
|
|
break;
|
|
case CannedMessagesSpec.TYPE_NEWSMS:
|
|
blobDBKey = "com.pebble.sendText";
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
int replies_length = -1;
|
|
|
|
for (String reply : cannedMessagesSpec.cannedMessages) {
|
|
replies_length += reply.getBytes().length + 1;
|
|
}
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(12 + replies_length);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
buf.putInt(0x00000000); // unknown
|
|
buf.put((byte) 0x00); // attributes count?
|
|
buf.put((byte) 0x01); // actions count?
|
|
|
|
// action
|
|
buf.put((byte) 0x00); // action id
|
|
buf.put((byte) 0x03); // action type = reply
|
|
buf.put((byte) 0x01); // attributes count
|
|
buf.put((byte) 0x08); // canned messages
|
|
buf.putShort((short) replies_length);
|
|
for (int i = 0; i < cannedMessagesSpec.cannedMessages.length - 1; i++) {
|
|
buf.put(cannedMessagesSpec.cannedMessages[i].getBytes());
|
|
buf.put((byte) 0x00);
|
|
}
|
|
// last one must not be zero terminated, else we get an additional empty reply
|
|
buf.put(cannedMessagesSpec.cannedMessages[cannedMessagesSpec.cannedMessages.length - 1].getBytes());
|
|
|
|
return encodeBlobdb(blobDBKey, BLOBDB_INSERT, BLOBDB_CANNED_MESSAGES, buf.array());
|
|
}
|
|
|
|
/* pebble specific install methods */
|
|
byte[] encodeUploadStart(byte type, int app_id, int size, String filename) {
|
|
short length;
|
|
if (mFwMajor >= 3 && (type != PUTBYTES_TYPE_FILE)) {
|
|
length = (short) 10;
|
|
type |= 0b10000000;
|
|
} else {
|
|
length = (short) 7;
|
|
}
|
|
|
|
if (type == PUTBYTES_TYPE_FILE && filename != null) {
|
|
length += (short) filename.getBytes().length + 1;
|
|
}
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(length);
|
|
buf.putShort(ENDPOINT_PUTBYTES);
|
|
buf.put(PUTBYTES_INIT);
|
|
buf.putInt(size);
|
|
buf.put(type);
|
|
|
|
if (mFwMajor >= 3 && (type != PUTBYTES_TYPE_FILE)) {
|
|
buf.putInt(app_id);
|
|
} else {
|
|
// slot
|
|
buf.put((byte) app_id);
|
|
}
|
|
|
|
if (type == PUTBYTES_TYPE_FILE && filename != null) {
|
|
buf.put(filename.getBytes());
|
|
buf.put((byte) 0);
|
|
}
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeUploadChunk(int token, byte[] buffer, int size) {
|
|
final short LENGTH_UPLOADCHUNK = 9;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCHUNK + size);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) (LENGTH_UPLOADCHUNK + size));
|
|
buf.putShort(ENDPOINT_PUTBYTES);
|
|
buf.put(PUTBYTES_SEND);
|
|
buf.putInt(token);
|
|
buf.putInt(size);
|
|
buf.put(buffer, 0, size);
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeUploadCommit(int token, int crc) {
|
|
final short LENGTH_UPLOADCOMMIT = 9;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCOMMIT);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_UPLOADCOMMIT);
|
|
buf.putShort(ENDPOINT_PUTBYTES);
|
|
buf.put(PUTBYTES_COMMIT);
|
|
buf.putInt(token);
|
|
buf.putInt(crc);
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeUploadComplete(int token) {
|
|
final short LENGTH_UPLOADCOMPLETE = 5;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCOMPLETE);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_UPLOADCOMPLETE);
|
|
buf.putShort(ENDPOINT_PUTBYTES);
|
|
buf.put(PUTBYTES_COMPLETE);
|
|
buf.putInt(token);
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeUploadCancel(int token) {
|
|
final short LENGTH_UPLOADCANCEL = 5;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCANCEL);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_UPLOADCANCEL);
|
|
buf.putShort(ENDPOINT_PUTBYTES);
|
|
buf.put(PUTBYTES_ABORT);
|
|
buf.putInt(token);
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeSystemMessage(byte systemMessage) {
|
|
final short LENGTH_SYSTEMMESSAGE = 2;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SYSTEMMESSAGE);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_SYSTEMMESSAGE);
|
|
buf.putShort(ENDPOINT_SYSTEMMESSAGE);
|
|
buf.put((byte) 0);
|
|
buf.put(systemMessage);
|
|
return buf.array();
|
|
|
|
}
|
|
|
|
byte[] encodeInstallFirmwareStart() {
|
|
return encodeSystemMessage(SYSTEMMESSAGE_FIRMWARESTART);
|
|
}
|
|
|
|
byte[] encodeInstallFirmwareComplete() {
|
|
return encodeSystemMessage(SYSTEMMESSAGE_FIRMWARECOMPLETE);
|
|
}
|
|
|
|
public byte[] encodeInstallFirmwareError() {
|
|
return encodeSystemMessage(SYSTEMMESSAGE_FIRMWAREFAIL);
|
|
}
|
|
|
|
|
|
byte[] encodeAppRefresh(int index) {
|
|
final short LENGTH_REFRESHAPP = 5;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_REFRESHAPP);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_REFRESHAPP);
|
|
buf.putShort(ENDPOINT_APPMANAGER);
|
|
buf.put(APPMANAGER_REFRESHAPP);
|
|
buf.putInt(index);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodeDatalog(byte handle, byte reply) {
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + 2);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) 2);
|
|
buf.putShort(ENDPOINT_DATALOG);
|
|
buf.put(reply);
|
|
buf.put(handle);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeApplicationMessageAck(UUID uuid, byte id) {
|
|
if (uuid == null) {
|
|
uuid = currentRunningApp;
|
|
}
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + 18); // +ACK
|
|
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) 18);
|
|
buf.putShort(ENDPOINT_APPLICATIONMESSAGE);
|
|
buf.put(APPLICATIONMESSAGE_ACK);
|
|
buf.put(id);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private byte[] encodePing(byte command, int cookie) {
|
|
final short LENGTH_PING = 5;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_PING);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_PING);
|
|
buf.putShort(ENDPOINT_PING);
|
|
buf.put(command);
|
|
buf.putInt(cookie);
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeEnableAppLogs(boolean enable) {
|
|
final short LENGTH_APPLOGS = 1;
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_APPLOGS);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort(LENGTH_APPLOGS);
|
|
buf.putShort(ENDPOINT_APPLOGS);
|
|
buf.put((byte) (enable ? 1 : 0));
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
private ArrayList<Pair<Integer, Object>> decodeDict(ByteBuffer buf) {
|
|
ArrayList<Pair<Integer, Object>> dict = new ArrayList<>();
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
byte dictSize = buf.get();
|
|
while (dictSize-- > 0) {
|
|
Integer key = buf.getInt();
|
|
byte type = buf.get();
|
|
short length = buf.getShort();
|
|
switch (type) {
|
|
case TYPE_INT:
|
|
case TYPE_UINT:
|
|
if (length == 1) {
|
|
dict.add(new Pair<Integer, Object>(key, buf.get()));
|
|
} else if (length == 2) {
|
|
dict.add(new Pair<Integer, Object>(key, buf.getShort()));
|
|
} else {
|
|
dict.add(new Pair<Integer, Object>(key, buf.getInt()));
|
|
}
|
|
break;
|
|
case TYPE_CSTRING:
|
|
case TYPE_BYTEARRAY:
|
|
byte[] bytes = new byte[length];
|
|
buf.get(bytes);
|
|
if (type == TYPE_BYTEARRAY) {
|
|
dict.add(new Pair<Integer, Object>(key, bytes));
|
|
} else {
|
|
dict.add(new Pair<Integer, Object>(key, new String(bytes)));
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
return dict;
|
|
}
|
|
|
|
private GBDeviceEvent[] decodeDictToJSONAppMessage(UUID uuid, ByteBuffer buf) throws JSONException {
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
byte dictSize = buf.get();
|
|
if (dictSize == 0) {
|
|
LOG.info("dict size is 0, ignoring");
|
|
return null;
|
|
}
|
|
JSONArray jsonArray = new JSONArray();
|
|
while (dictSize-- > 0) {
|
|
JSONObject jsonObject = new JSONObject();
|
|
Integer key = buf.getInt();
|
|
byte type = buf.get();
|
|
short length = buf.getShort();
|
|
jsonObject.put("key", key);
|
|
if (type == TYPE_CSTRING) {
|
|
length--;
|
|
}
|
|
jsonObject.put("length", length);
|
|
switch (type) {
|
|
case TYPE_UINT:
|
|
jsonObject.put("type", "uint");
|
|
if (length == 1) {
|
|
jsonObject.put("value", buf.get() & 0xff);
|
|
} else if (length == 2) {
|
|
jsonObject.put("value", buf.getShort() & 0xffff);
|
|
} else {
|
|
jsonObject.put("value", buf.getInt() & 0xffffffffL);
|
|
}
|
|
break;
|
|
case TYPE_INT:
|
|
jsonObject.put("type", "int");
|
|
if (length == 1) {
|
|
jsonObject.put("value", buf.get());
|
|
} else if (length == 2) {
|
|
jsonObject.put("value", buf.getShort());
|
|
} else {
|
|
jsonObject.put("value", buf.getInt());
|
|
}
|
|
break;
|
|
case TYPE_BYTEARRAY:
|
|
case TYPE_CSTRING:
|
|
byte[] bytes = new byte[length];
|
|
buf.get(bytes);
|
|
if (type == TYPE_BYTEARRAY) {
|
|
jsonObject.put("type", "bytes");
|
|
jsonObject.put("value", new String(Base64.encode(bytes, Base64.NO_WRAP)));
|
|
} else {
|
|
jsonObject.put("type", "string");
|
|
jsonObject.put("value", new String(bytes));
|
|
buf.get(); // skip null-termination;
|
|
}
|
|
break;
|
|
default:
|
|
LOG.info("unknown type in appmessage, ignoring");
|
|
return null;
|
|
}
|
|
jsonArray.put(jsonObject);
|
|
}
|
|
|
|
GBDeviceEventSendBytes sendBytesAck = null;
|
|
if (mAlwaysACKPebbleKit) {
|
|
// this is a hack we send an ack to the Pebble immediately because somebody said it helps some PebbleKit apps :P
|
|
sendBytesAck = new GBDeviceEventSendBytes();
|
|
sendBytesAck.encodedBytes = encodeApplicationMessageAck(uuid, last_id);
|
|
}
|
|
GBDeviceEventAppMessage appMessage = new GBDeviceEventAppMessage();
|
|
appMessage.appUUID = uuid;
|
|
appMessage.id = last_id & 0xff;
|
|
appMessage.message = jsonArray.toString();
|
|
return new GBDeviceEvent[]{appMessage, sendBytesAck};
|
|
}
|
|
|
|
byte[] encodeApplicationMessagePush(short endpoint, UUID uuid, ArrayList<Pair<Integer, Object>> pairs, Integer ext_id) {
|
|
int length = LENGTH_UUID + 3; // UUID + (PUSH + id + length of dict)
|
|
for (Pair<Integer, Object> pair : pairs) {
|
|
if (pair.first == null || pair.second == null)
|
|
continue;
|
|
length += 7; // key + type + length
|
|
if (pair.second instanceof Integer) {
|
|
length += 4;
|
|
} else if (pair.second instanceof Short) {
|
|
length += 2;
|
|
} else if (pair.second instanceof Byte) {
|
|
length += 1;
|
|
} else if (pair.second instanceof String) {
|
|
length += ((String) pair.second).getBytes().length + 1;
|
|
} else if (pair.second instanceof byte[]) {
|
|
length += ((byte[]) pair.second).length;
|
|
} else {
|
|
LOG.warn("unknown type: " + pair.second.getClass().toString());
|
|
}
|
|
}
|
|
|
|
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
buf.putShort((short) length);
|
|
buf.putShort(endpoint); // 48 or 49
|
|
buf.put(APPLICATIONMESSAGE_PUSH);
|
|
buf.put(++last_id);
|
|
buf.putLong(uuid.getMostSignificantBits());
|
|
buf.putLong(uuid.getLeastSignificantBits());
|
|
buf.put((byte) pairs.size());
|
|
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
for (Pair<Integer, Object> pair : pairs) {
|
|
if (pair.first == null || pair.second == null)
|
|
continue;
|
|
buf.putInt(pair.first);
|
|
if (pair.second instanceof Integer) {
|
|
buf.put(TYPE_INT);
|
|
buf.putShort((short) 4); // length
|
|
buf.putInt((int) pair.second);
|
|
} else if (pair.second instanceof Short) {
|
|
buf.put(TYPE_INT);
|
|
buf.putShort((short) 2); // length
|
|
buf.putShort((short) pair.second);
|
|
} else if (pair.second instanceof Byte) {
|
|
buf.put(TYPE_INT);
|
|
buf.putShort((short) 1); // length
|
|
buf.put((byte) pair.second);
|
|
} else if (pair.second instanceof String) {
|
|
String str = (String) pair.second;
|
|
buf.put(TYPE_CSTRING);
|
|
buf.putShort((short) (str.getBytes().length + 1));
|
|
buf.put(str.getBytes());
|
|
buf.put((byte) 0);
|
|
} else if (pair.second instanceof byte[]) {
|
|
byte[] bytes = (byte[]) pair.second;
|
|
buf.put(TYPE_BYTEARRAY);
|
|
buf.putShort((short) bytes.length);
|
|
buf.put(bytes);
|
|
}
|
|
}
|
|
|
|
idLookup[last_id & 0xff] = ext_id;
|
|
|
|
return buf.array();
|
|
}
|
|
|
|
byte[] encodeApplicationMessageFromJSON(UUID uuid, JSONArray jsonArray) {
|
|
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
|
|
for (int i = 0; i < jsonArray.length(); i++) {
|
|
try {
|
|
JSONObject jsonObject = (JSONObject) jsonArray.get(i);
|
|
String type = (String) jsonObject.get("type");
|
|
int key = jsonObject.getInt("key");
|
|
int length = jsonObject.getInt("length");
|
|
switch (type) {
|
|
case "uint":
|
|
case "int":
|
|
if (length == 1) {
|
|
pairs.add(new Pair<>(key, (Object) (byte) jsonObject.getInt("value")));
|
|
} else if (length == 2) {
|
|
pairs.add(new Pair<>(key, (Object) (short) jsonObject.getInt("value")));
|
|
} else {
|
|
if (type.equals("uint")) {
|
|
pairs.add(new Pair<>(key, (Object) (int) (jsonObject.getInt("value") & 0xffffffffL)));
|
|
} else {
|
|
pairs.add(new Pair<>(key, (Object) jsonObject.getInt("value")));
|
|
}
|
|
}
|
|
break;
|
|
case "string":
|
|
pairs.add(new Pair<>(key, (Object) jsonObject.getString("value")));
|
|
break;
|
|
case "bytes":
|
|
byte[] bytes = Base64.decode(jsonObject.getString("value"), Base64.NO_WRAP);
|
|
pairs.add(new Pair<>(key, (Object) bytes));
|
|
break;
|
|
}
|
|
} catch (JSONException e) {
|
|
LOG.error("error decoding JSON", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return encodeApplicationMessagePush(ENDPOINT_APPLICATIONMESSAGE, uuid, pairs, null);
|
|
}
|
|
|
|
private byte reverseBits(byte in) {
|
|
byte out = 0;
|
|
for (int i = 0; i < 8; i++) {
|
|
byte bit = (byte) (in & 1);
|
|
out = (byte) ((out << 1) | bit);
|
|
in = (byte) (in >> 1);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private GBDeviceEventScreenshot decodeScreenshot(ByteBuffer buf, int length) {
|
|
if (mDevEventScreenshot == null) {
|
|
byte result = buf.get();
|
|
mDevEventScreenshot = new GBDeviceEventScreenshot();
|
|
int version = buf.getInt();
|
|
if (result != 0) {
|
|
return null;
|
|
}
|
|
mDevEventScreenshot.width = buf.getInt();
|
|
mDevEventScreenshot.height = buf.getInt();
|
|
|
|
if (version == 1) {
|
|
mDevEventScreenshot.bpp = 1;
|
|
mDevEventScreenshot.clut = clut_pebble;
|
|
} else {
|
|
mDevEventScreenshot.bpp = 8;
|
|
mDevEventScreenshot.clut = clut_pebbletime;
|
|
}
|
|
|
|
mScreenshotRemaining = (mDevEventScreenshot.width * mDevEventScreenshot.height * mDevEventScreenshot.bpp) / 8;
|
|
|
|
mDevEventScreenshot.data = new byte[mScreenshotRemaining];
|
|
length -= 13;
|
|
}
|
|
if (mScreenshotRemaining == -1) {
|
|
return null;
|
|
}
|
|
for (int i = 0; i < length; i++) {
|
|
byte corrected = buf.get();
|
|
if (mDevEventScreenshot.bpp == 1) {
|
|
corrected = reverseBits(corrected);
|
|
} else {
|
|
corrected = (byte) (corrected & 0b00111111);
|
|
}
|
|
|
|
mDevEventScreenshot.data[mDevEventScreenshot.data.length - mScreenshotRemaining + i] = corrected;
|
|
}
|
|
mScreenshotRemaining -= length;
|
|
LOG.info("Screenshot remaining bytes " + mScreenshotRemaining);
|
|
if (mScreenshotRemaining == 0) {
|
|
mScreenshotRemaining = -1;
|
|
LOG.info("Got screenshot : " + mDevEventScreenshot.width + "x" + mDevEventScreenshot.height + " " + "pixels");
|
|
GBDeviceEventScreenshot devEventScreenshot = mDevEventScreenshot;
|
|
mDevEventScreenshot = null;
|
|
return devEventScreenshot;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEvent[] decodeAction(ByteBuffer buf) {
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
byte command = buf.get();
|
|
if (command == NOTIFICATIONACTION_INVOKE) {
|
|
int id;
|
|
UUID uuid = new UUID(0,0);
|
|
if (mFwMajor >= 3) {
|
|
uuid = getUUID(buf);
|
|
id = (int) (uuid.getLeastSignificantBits() & 0xffffffffL);
|
|
} else {
|
|
id = buf.getInt();
|
|
}
|
|
byte action = buf.get();
|
|
if (action >= 0x00 && action <= 0xf) {
|
|
GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl();
|
|
devEvtNotificationControl.handle = id;
|
|
String caption = "undefined";
|
|
int icon_id = 1;
|
|
boolean needsAck2x = true;
|
|
switch (action) {
|
|
case 0x01:
|
|
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.OPEN;
|
|
caption = "Opened";
|
|
icon_id = PebbleIconID.DURING_PHONE_CALL;
|
|
break;
|
|
case 0x02:
|
|
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS;
|
|
caption = "Dismissed";
|
|
icon_id = PebbleIconID.RESULT_DISMISSED;
|
|
needsAck2x = false;
|
|
break;
|
|
case 0x03:
|
|
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS_ALL;
|
|
caption = "All dismissed";
|
|
icon_id = PebbleIconID.RESULT_DISMISSED;
|
|
needsAck2x = false;
|
|
break;
|
|
case 0x04:
|
|
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.MUTE;
|
|
caption = "Muted";
|
|
icon_id = PebbleIconID.RESULT_MUTE;
|
|
break;
|
|
//TODO: 0x05 is not a special case anymore, and reply action might have an index that is higher. see default below
|
|
case 0x00:
|
|
default:
|
|
boolean failed = true;
|
|
byte attribute_count = buf.get();
|
|
if (attribute_count > 0) {
|
|
byte attribute = buf.get();
|
|
if (attribute == 0x01) { // reply string is in attribute 0x01
|
|
short length = buf.getShort();
|
|
if (length > 64) length = 64;
|
|
byte[] reply = new byte[length];
|
|
buf.get(reply);
|
|
devEvtNotificationControl.phoneNumber = null;
|
|
if (buf.remaining() > 1 && buf.get() == 0x0c) {
|
|
short phoneNumberLength = buf.getShort();
|
|
byte[] phoneNumberBytes = new byte[phoneNumberLength];
|
|
buf.get(phoneNumberBytes);
|
|
devEvtNotificationControl.phoneNumber = new String(phoneNumberBytes);
|
|
}
|
|
devEvtNotificationControl.reply = new String(reply);
|
|
caption = "SENT";
|
|
icon_id = PebbleIconID.RESULT_SENT;
|
|
failed = false;
|
|
}
|
|
} else {
|
|
icon_id = PebbleIconID.GENERIC_CONFIRMATION;
|
|
caption = "EXECUTED";
|
|
failed = false;
|
|
}
|
|
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
|
|
devEvtNotificationControl.handle = (devEvtNotificationControl.handle << 4) + action - 0x04;
|
|
if (failed) {
|
|
caption = "FAILED";
|
|
icon_id = PebbleIconID.RESULT_FAILED;
|
|
devEvtNotificationControl = null; // error
|
|
}
|
|
break;
|
|
}
|
|
GBDeviceEventSendBytes sendBytesAck = null;
|
|
if (mFwMajor >= 3 || needsAck2x) {
|
|
sendBytesAck = new GBDeviceEventSendBytes();
|
|
if (mFwMajor >= 3) {
|
|
sendBytesAck.encodedBytes = encodeActionResponse(uuid, icon_id, caption);
|
|
} else {
|
|
sendBytesAck.encodedBytes = encodeActionResponse2x(id, action, 6, caption);
|
|
}
|
|
}
|
|
return new GBDeviceEvent[]{sendBytesAck, devEvtNotificationControl};
|
|
}
|
|
LOG.info("unexpected action: " + action);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEventSendBytes decodePing(ByteBuffer buf) {
|
|
byte command = buf.get();
|
|
if (command == PING_PING) {
|
|
int cookie = buf.getInt();
|
|
LOG.info("Received PING - will reply");
|
|
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
|
|
sendBytes.encodedBytes = encodePing(PING_PONG, cookie);
|
|
return sendBytes;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void decodeAppLogs(ByteBuffer buf) {
|
|
UUID uuid = getUUID(buf);
|
|
int timestamp = buf.getInt();
|
|
int logLevel = buf.get() & 0xff;
|
|
int messageLength = buf.get() & 0xff;
|
|
int lineNumber = buf.getShort() & 0xffff;
|
|
String fileName = getFixedString(buf, 16);
|
|
String message = getFixedString(buf, messageLength);
|
|
LOG.debug("APP_LOGS (" + logLevel +") from uuid " + uuid.toString() + " in " + fileName + ":" + lineNumber + " " + message);
|
|
}
|
|
|
|
private GBDeviceEvent decodeSystemMessage(ByteBuffer buf) {
|
|
buf.get(); // unknown;
|
|
byte command = buf.get();
|
|
final String ENDPOINT_NAME = "SYSTEM MESSAGE";
|
|
switch (command) {
|
|
case SYSTEMMESSAGE_STOPRECONNECTING:
|
|
LOG.info(ENDPOINT_NAME + ": stop reconnecting");
|
|
break;
|
|
case SYSTEMMESSAGE_STARTRECONNECTING:
|
|
LOG.info(ENDPOINT_NAME + ": start reconnecting");
|
|
break;
|
|
default:
|
|
LOG.info(ENDPOINT_NAME + ": " + command);
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEvent[] decodeAppRunState(ByteBuffer buf) {
|
|
byte command = buf.get();
|
|
UUID uuid = getUUID(buf);
|
|
final String ENDPOINT_NAME = "APPRUNSTATE";
|
|
switch (command) {
|
|
case APPRUNSTATE_START:
|
|
LOG.info(ENDPOINT_NAME + ": started " + uuid);
|
|
|
|
AppMessageHandler handler = mAppMessageHandlers.get(uuid);
|
|
if (handler != null) {
|
|
currentRunningApp = uuid;
|
|
return handler.onAppStart();
|
|
}
|
|
else {
|
|
if (!uuid.equals(currentRunningApp)) {
|
|
currentRunningApp = uuid;
|
|
GBDeviceEventAppManagement gbDeviceEventAppManagement = new GBDeviceEventAppManagement();
|
|
gbDeviceEventAppManagement.uuid = uuid;
|
|
gbDeviceEventAppManagement.type = GBDeviceEventAppManagement.EventType.START;
|
|
gbDeviceEventAppManagement.event = GBDeviceEventAppManagement.Event.SUCCESS;
|
|
return new GBDeviceEvent[]{gbDeviceEventAppManagement};
|
|
}
|
|
}
|
|
break;
|
|
case APPRUNSTATE_STOP:
|
|
LOG.info(ENDPOINT_NAME + ": stopped " + uuid);
|
|
|
|
GBDeviceEventAppManagement gbDeviceEventAppManagement = new GBDeviceEventAppManagement();
|
|
gbDeviceEventAppManagement.uuid = uuid;
|
|
gbDeviceEventAppManagement.type = GBDeviceEventAppManagement.EventType.STOP;
|
|
gbDeviceEventAppManagement.event = GBDeviceEventAppManagement.Event.SUCCESS;
|
|
return new GBDeviceEvent[]{gbDeviceEventAppManagement};
|
|
default:
|
|
LOG.info(ENDPOINT_NAME + ": (cmd:" + command + ")" + uuid);
|
|
break;
|
|
}
|
|
return new GBDeviceEvent[]{null};
|
|
}
|
|
|
|
private GBDeviceEvent decodeBlobDb(ByteBuffer buf) {
|
|
final String ENDPOINT_NAME = "BLOBDB";
|
|
final String statusString[] = {
|
|
"unknown",
|
|
"success",
|
|
"general failure",
|
|
"invalid operation",
|
|
"invalid database id",
|
|
"invalid data",
|
|
"key does not exist",
|
|
"database full",
|
|
"data stale",
|
|
};
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
short token = buf.getShort();
|
|
byte status = buf.get();
|
|
|
|
if (status >= 0 && status < statusString.length) {
|
|
LOG.info(ENDPOINT_NAME + ": " + statusString[status] + " (token " + (token & 0xffff) + ")");
|
|
} else {
|
|
LOG.warn(ENDPOINT_NAME + ": unknown status " + status + " (token " + (token & 0xffff) + ")");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEventAppManagement decodeAppFetch(ByteBuffer buf) {
|
|
byte command = buf.get();
|
|
if (command == 0x01) {
|
|
UUID uuid = getUUID(buf);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
int app_id = buf.getInt();
|
|
GBDeviceEventAppManagement fetchRequest = new GBDeviceEventAppManagement();
|
|
fetchRequest.type = GBDeviceEventAppManagement.EventType.INSTALL;
|
|
fetchRequest.event = GBDeviceEventAppManagement.Event.REQUEST;
|
|
fetchRequest.token = app_id;
|
|
fetchRequest.uuid = uuid;
|
|
return fetchRequest;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEvent[] decodeDatalog(ByteBuffer buf, short length) {
|
|
byte command = buf.get();
|
|
byte id = buf.get();
|
|
GBDeviceEvent[] devEvtsDataLogging = null;
|
|
switch (command) {
|
|
case DATALOG_TIMEOUT:
|
|
LOG.info("DATALOG TIMEOUT. id=" + (id & 0xff) + " - ignoring");
|
|
return null;
|
|
case DATALOG_SENDDATA:
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
int items_left = buf.getInt();
|
|
int crc = buf.getInt();
|
|
DatalogSession datalogSession = mDatalogSessions.get(id);
|
|
LOG.info("DATALOG SENDDATA. id=" + (id & 0xff) + ", items_left=" + items_left + ", total length=" + (length - 10));
|
|
if (datalogSession != null) {
|
|
LOG.info("DATALOG UUID=" + datalogSession.uuid + ", tag=" + datalogSession.tag + datalogSession.getTaginfo() + ", itemSize=" + datalogSession.itemSize + ", itemType=" + datalogSession.itemType);
|
|
if (!datalogSession.uuid.equals(UUID_ZERO) && datalogSession.getClass().equals(DatalogSession.class) && mEnablePebbleKit) {
|
|
devEvtsDataLogging = datalogSession.handleMessageForPebbleKit(buf, length - 10);
|
|
} else {
|
|
devEvtsDataLogging = datalogSession.handleMessage(buf, length - 10);
|
|
}
|
|
}
|
|
break;
|
|
case DATALOG_OPENSESSION:
|
|
UUID uuid = getUUID(buf);
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
int timestamp = buf.getInt();
|
|
int log_tag = buf.getInt();
|
|
byte item_type = buf.get();
|
|
short item_size = buf.getShort();
|
|
LOG.info("DATALOG OPENSESSION. id=" + (id & 0xff) + ", App UUID=" + uuid.toString() + ", log_tag=" + log_tag + ", item_type=" + item_type + ", itemSize=" + item_size);
|
|
if (!mDatalogSessions.containsKey(id)) {
|
|
if (uuid.equals(UUID_ZERO) && log_tag == 78) {
|
|
mDatalogSessions.put(id, new DatalogSessionAnalytics(id, uuid, timestamp, log_tag, item_type, item_size, getDevice()));
|
|
} else if (uuid.equals(UUID_ZERO) && log_tag == 81) {
|
|
mDatalogSessions.put(id, new DatalogSessionHealthSteps(id, uuid, timestamp, log_tag, item_type, item_size, getDevice()));
|
|
} else if (uuid.equals(UUID_ZERO) && log_tag == 83) {
|
|
mDatalogSessions.put(id, new DatalogSessionHealthSleep(id, uuid, timestamp, log_tag, item_type, item_size, getDevice()));
|
|
} else if (uuid.equals(UUID_ZERO) && log_tag == 84) {
|
|
mDatalogSessions.put(id, new DatalogSessionHealthOverlayData(id, uuid, timestamp, log_tag, item_type, item_size, getDevice()));
|
|
} else if (uuid.equals(UUID_ZERO) && log_tag == 85) {
|
|
mDatalogSessions.put(id, new DatalogSessionHealthHR(id, uuid, timestamp, log_tag, item_type, item_size, getDevice()));
|
|
} else {
|
|
mDatalogSessions.put(id, new DatalogSession(id, uuid, timestamp, log_tag, item_type, item_size));
|
|
}
|
|
}
|
|
devEvtsDataLogging = new GBDeviceEvent[]{null};
|
|
break;
|
|
case DATALOG_CLOSE:
|
|
LOG.info("DATALOG_CLOSE. id=" + (id & 0xff));
|
|
datalogSession = mDatalogSessions.get(id);
|
|
if (datalogSession != null) {
|
|
if (!datalogSession.uuid.equals(UUID_ZERO) && datalogSession.getClass().equals(DatalogSession.class) && mEnablePebbleKit) {
|
|
GBDeviceEventDataLogging dataLogging = new GBDeviceEventDataLogging();
|
|
dataLogging.command = GBDeviceEventDataLogging.COMMAND_FINISH_SESSION;
|
|
dataLogging.appUUID = datalogSession.uuid;
|
|
dataLogging.tag = datalogSession.tag;
|
|
devEvtsDataLogging = new GBDeviceEvent[]{dataLogging, null};
|
|
}
|
|
if (datalogSession.uuid.equals(UUID_ZERO) && (datalogSession.tag == 81 || datalogSession.tag == 83 || datalogSession.tag == 84)) {
|
|
GB.signalActivityDataFinish();
|
|
}
|
|
mDatalogSessions.remove(id);
|
|
}
|
|
break;
|
|
default:
|
|
LOG.info("unknown DATALOG command: " + (command & 0xff));
|
|
break;
|
|
}
|
|
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
|
|
|
|
if (devEvtsDataLogging != null) {
|
|
// append ack
|
|
LOG.info("sending ACK (0x85)");
|
|
sendBytes.encodedBytes = encodeDatalog(id, DATALOG_ACK);
|
|
devEvtsDataLogging[devEvtsDataLogging.length - 1] = sendBytes;
|
|
} else {
|
|
LOG.info("sending NACK (0x86)");
|
|
sendBytes.encodedBytes = encodeDatalog(id, DATALOG_NACK);
|
|
devEvtsDataLogging = new GBDeviceEvent[]{sendBytes};
|
|
}
|
|
return devEvtsDataLogging;
|
|
}
|
|
|
|
private GBDeviceEvent decodeAppReorder(ByteBuffer buf) {
|
|
byte status = buf.get();
|
|
if (status == 1) {
|
|
LOG.info("app reordering successful");
|
|
} else {
|
|
LOG.info("app reordering returned status " + status);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GBDeviceEvent decodeVoiceControl(ByteBuffer buf) {
|
|
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
byte command = buf.get();
|
|
int flags = buf.getInt();
|
|
byte session_type = buf.get(); //0x01 dictation 0x02 command
|
|
short session_id = buf.getShort();
|
|
//attributes
|
|
byte count = buf.get();
|
|
byte type = buf.get();
|
|
short length = buf.getShort();
|
|
byte[] version = new byte[20];
|
|
buf.get(version); //it's a string like "1.2rc1"
|
|
int sample_rate = buf.getInt();
|
|
short bit_rate = buf.getShort();
|
|
byte bitstream_version = buf.get();
|
|
short frame_size = buf.getShort();
|
|
|
|
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
|
|
if (command == 0x01) { //session setup
|
|
int replLenght = 7;
|
|
byte replStatus = 5; // 5 = disabled, change to 0 to send success
|
|
ByteBuffer repl = ByteBuffer.allocate(LENGTH_PREFIX + replLenght);
|
|
repl.order(ByteOrder.BIG_ENDIAN);
|
|
repl.putShort((short) replLenght);
|
|
repl.putShort(ENDPOINT_VOICECONTROL);
|
|
repl.put(command);
|
|
repl.putInt(flags);
|
|
repl.put(session_type);
|
|
repl.put(replStatus);
|
|
|
|
sendBytes.encodedBytes = repl.array();
|
|
|
|
} else if (command == 0x02) { //dictation result (possibly it is something we send, not something we receive)
|
|
sendBytes.encodedBytes = null;
|
|
}
|
|
return sendBytes;
|
|
}
|
|
|
|
private GBDeviceEvent decodeAudioStream(ByteBuffer buf) {
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
|
ByteBuffer buf = ByteBuffer.wrap(responseData);
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
short length = buf.getShort();
|
|
short endpoint = buf.getShort();
|
|
GBDeviceEvent devEvts[] = null;
|
|
byte pebbleCmd;
|
|
switch (endpoint) {
|
|
case ENDPOINT_MUSICCONTROL:
|
|
pebbleCmd = buf.get();
|
|
GBDeviceEventMusicControl musicCmd = new GBDeviceEventMusicControl();
|
|
switch (pebbleCmd) {
|
|
case MUSICCONTROL_NEXT:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.NEXT;
|
|
break;
|
|
case MUSICCONTROL_PREVIOUS:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.PREVIOUS;
|
|
break;
|
|
case MUSICCONTROL_PLAY:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.PLAY;
|
|
break;
|
|
case MUSICCONTROL_PAUSE:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.PAUSE;
|
|
break;
|
|
case MUSICCONTROL_PLAYPAUSE:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.PLAYPAUSE;
|
|
break;
|
|
case MUSICCONTROL_VOLUMEUP:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.VOLUMEUP;
|
|
break;
|
|
case MUSICCONTROL_VOLUMEDOWN:
|
|
musicCmd.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
devEvts = new GBDeviceEvent[]{musicCmd};
|
|
break;
|
|
case ENDPOINT_PHONECONTROL:
|
|
pebbleCmd = buf.get();
|
|
GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl();
|
|
switch (pebbleCmd) {
|
|
case PHONECONTROL_HANGUP:
|
|
callCmd.event = GBDeviceEventCallControl.Event.END;
|
|
break;
|
|
default:
|
|
LOG.info("Unknown PHONECONTROL event" + pebbleCmd);
|
|
break;
|
|
}
|
|
devEvts = new GBDeviceEvent[]{callCmd};
|
|
break;
|
|
case ENDPOINT_FIRMWAREVERSION:
|
|
pebbleCmd = buf.get();
|
|
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
|
|
|
buf.getInt(); // skip
|
|
versionCmd.fwVersion = getFixedString(buf, 32);
|
|
|
|
mFwMajor = versionCmd.fwVersion.charAt(1) - 48;
|
|
LOG.info("Pebble firmware major detected as " + mFwMajor);
|
|
|
|
byte[] tmp = new byte[9];
|
|
buf.get(tmp, 0, 9);
|
|
int hwRev = buf.get() + 8;
|
|
if (hwRev >= 0 && hwRev < hwRevisions.length) {
|
|
versionCmd.hwVersion = hwRevisions[hwRev];
|
|
}
|
|
devEvts = new GBDeviceEvent[]{versionCmd};
|
|
break;
|
|
case ENDPOINT_APPMANAGER:
|
|
pebbleCmd = buf.get();
|
|
switch (pebbleCmd) {
|
|
case APPMANAGER_GETAPPBANKSTATUS:
|
|
GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo();
|
|
int slotCount = buf.getInt();
|
|
int slotsUsed = buf.getInt();
|
|
appInfoCmd.apps = new GBDeviceApp[slotsUsed];
|
|
boolean[] slotInUse = new boolean[slotCount];
|
|
|
|
for (int i = 0; i < slotsUsed; i++) {
|
|
int id = buf.getInt();
|
|
int index = buf.getInt();
|
|
slotInUse[index] = true;
|
|
String appName = getFixedString(buf, 32);
|
|
String appCreator = getFixedString(buf, 32);
|
|
|
|
int flags = buf.getInt();
|
|
|
|
GBDeviceApp.Type appType;
|
|
if ((flags & 16) == 16) { // FIXME: verify this assumption
|
|
appType = GBDeviceApp.Type.APP_ACTIVITYTRACKER;
|
|
} else if ((flags & 1) == 1) { // FIXME: verify this assumption
|
|
appType = GBDeviceApp.Type.WATCHFACE;
|
|
} else {
|
|
appType = GBDeviceApp.Type.APP_GENERIC;
|
|
}
|
|
short appVersion = buf.getShort();
|
|
appInfoCmd.apps[i] = new GBDeviceApp(tmpUUIDS.get(i), appName, appCreator, String.valueOf(appVersion), appType);
|
|
}
|
|
for (int i = 0; i < slotCount; i++) {
|
|
if (!slotInUse[i]) {
|
|
appInfoCmd.freeSlot = (byte) i;
|
|
LOG.info("found free slot " + i);
|
|
break;
|
|
}
|
|
}
|
|
devEvts = new GBDeviceEvent[]{appInfoCmd};
|
|
break;
|
|
case APPMANAGER_GETUUIDS:
|
|
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
|
|
sendBytes.encodedBytes = encodeSimpleMessage(ENDPOINT_APPMANAGER, APPMANAGER_GETAPPBANKSTATUS);
|
|
devEvts = new GBDeviceEvent[]{sendBytes};
|
|
tmpUUIDS.clear();
|
|
slotsUsed = buf.getInt();
|
|
for (int i = 0; i < slotsUsed; i++) {
|
|
UUID uuid = getUUID(buf);
|
|
LOG.info("found uuid: " + uuid);
|
|
tmpUUIDS.add(uuid);
|
|
}
|
|
break;
|
|
case APPMANAGER_REMOVEAPP:
|
|
GBDeviceEventAppManagement deleteRes = new GBDeviceEventAppManagement();
|
|
deleteRes.type = GBDeviceEventAppManagement.EventType.DELETE;
|
|
|
|
int result = buf.getInt();
|
|
switch (result) {
|
|
case APPMANAGER_RES_SUCCESS:
|
|
deleteRes.event = GBDeviceEventAppManagement.Event.SUCCESS;
|
|
break;
|
|
default:
|
|
deleteRes.event = GBDeviceEventAppManagement.Event.FAILURE;
|
|
break;
|
|
}
|
|
devEvts = new GBDeviceEvent[]{deleteRes};
|
|
break;
|
|
default:
|
|
LOG.info("Unknown APPMANAGER event" + pebbleCmd);
|
|
break;
|
|
}
|
|
break;
|
|
case ENDPOINT_PUTBYTES:
|
|
pebbleCmd = buf.get();
|
|
GBDeviceEventAppManagement installRes = new GBDeviceEventAppManagement();
|
|
installRes.type = GBDeviceEventAppManagement.EventType.INSTALL;
|
|
switch (pebbleCmd) {
|
|
case PUTBYTES_INIT:
|
|
installRes.token = buf.getInt();
|
|
installRes.event = GBDeviceEventAppManagement.Event.SUCCESS;
|
|
break;
|
|
default:
|
|
installRes.token = buf.getInt();
|
|
installRes.event = GBDeviceEventAppManagement.Event.FAILURE;
|
|
break;
|
|
}
|
|
devEvts = new GBDeviceEvent[]{installRes};
|
|
break;
|
|
case ENDPOINT_APPLICATIONMESSAGE:
|
|
case ENDPOINT_LAUNCHER:
|
|
pebbleCmd = buf.get();
|
|
last_id = buf.get();
|
|
UUID uuid = getUUID(buf);
|
|
|
|
switch (pebbleCmd) {
|
|
case APPLICATIONMESSAGE_PUSH:
|
|
LOG.info((endpoint == ENDPOINT_LAUNCHER ? "got LAUNCHER PUSH from UUID : " : "got APPLICATIONMESSAGE PUSH from UUID : ") + uuid);
|
|
AppMessageHandler handler = mAppMessageHandlers.get(uuid);
|
|
if (handler != null) {
|
|
currentRunningApp = uuid;
|
|
if (handler.isEnabled()) {
|
|
if (endpoint == ENDPOINT_APPLICATIONMESSAGE) {
|
|
ArrayList<Pair<Integer, Object>> dict = decodeDict(buf);
|
|
devEvts = handler.handleMessage(dict);
|
|
}
|
|
else {
|
|
devEvts = handler.onAppStart();
|
|
}
|
|
} else {
|
|
devEvts = new GBDeviceEvent[]{null};
|
|
}
|
|
} else {
|
|
try {
|
|
devEvts = decodeDictToJSONAppMessage(uuid, buf);
|
|
} catch (JSONException e) {
|
|
LOG.error(e.getMessage());
|
|
}
|
|
if (!uuid.equals(currentRunningApp)) {
|
|
GBDeviceEventAppManagement gbDeviceEventAppManagement = new GBDeviceEventAppManagement();
|
|
gbDeviceEventAppManagement.uuid = uuid;
|
|
gbDeviceEventAppManagement.type = GBDeviceEventAppManagement.EventType.START;
|
|
gbDeviceEventAppManagement.event = GBDeviceEventAppManagement.Event.SUCCESS;
|
|
|
|
// prepend the
|
|
GBDeviceEvent concatEvents[] = new GBDeviceEvent[(devEvts != null ? devEvts.length : 0) + 1];
|
|
concatEvents[0] = gbDeviceEventAppManagement;
|
|
if (devEvts != null) {
|
|
System.arraycopy(devEvts, 0, concatEvents, 1, devEvts.length);
|
|
}
|
|
devEvts = concatEvents;
|
|
}
|
|
}
|
|
currentRunningApp = uuid;
|
|
break;
|
|
case APPLICATIONMESSAGE_ACK:
|
|
case APPLICATIONMESSAGE_NACK:
|
|
if (pebbleCmd == APPLICATIONMESSAGE_ACK) {
|
|
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") ACK");
|
|
} else {
|
|
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") NACK");
|
|
}
|
|
GBDeviceEventAppMessage evtAppMessage = null;
|
|
if (endpoint == ENDPOINT_APPLICATIONMESSAGE && idLookup[last_id & 0xff] != null) {
|
|
evtAppMessage = new GBDeviceEventAppMessage();
|
|
if (pebbleCmd == APPLICATIONMESSAGE_ACK) {
|
|
evtAppMessage.type = GBDeviceEventAppMessage.TYPE_ACK;
|
|
} else {
|
|
evtAppMessage.type = GBDeviceEventAppMessage.TYPE_NACK;
|
|
}
|
|
evtAppMessage.id = idLookup[last_id & 0xff];
|
|
evtAppMessage.appUUID = currentRunningApp;
|
|
}
|
|
devEvts = new GBDeviceEvent[]{evtAppMessage};
|
|
break;
|
|
case APPLICATIONMESSAGE_REQUEST:
|
|
LOG.info("got APPLICATIONMESSAGE/LAUNCHER (EP " + endpoint + ") REQUEST");
|
|
devEvts = new GBDeviceEvent[]{null};
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case ENDPOINT_PHONEVERSION:
|
|
pebbleCmd = buf.get();
|
|
switch (pebbleCmd) {
|
|
case PHONEVERSION_REQUEST:
|
|
LOG.info("Pebble asked for Phone/App Version - repLYING!");
|
|
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
|
|
sendBytes.encodedBytes = encodePhoneVersion(PHONEVERSION_REMOTE_OS_ANDROID);
|
|
devEvts = new GBDeviceEvent[]{sendBytes};
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case ENDPOINT_DATALOG:
|
|
devEvts = decodeDatalog(buf, length);
|
|
break;
|
|
case ENDPOINT_SCREENSHOT:
|
|
devEvts = new GBDeviceEvent[]{decodeScreenshot(buf, length)};
|
|
break;
|
|
case ENDPOINT_EXTENSIBLENOTIFS:
|
|
case ENDPOINT_NOTIFICATIONACTION:
|
|
devEvts = decodeAction(buf);
|
|
break;
|
|
case ENDPOINT_PING:
|
|
devEvts = new GBDeviceEvent[]{decodePing(buf)};
|
|
break;
|
|
case ENDPOINT_APPFETCH:
|
|
devEvts = new GBDeviceEvent[]{decodeAppFetch(buf)};
|
|
break;
|
|
case ENDPOINT_SYSTEMMESSAGE:
|
|
devEvts = new GBDeviceEvent[]{decodeSystemMessage(buf)};
|
|
break;
|
|
case ENDPOINT_APPRUNSTATE:
|
|
devEvts = decodeAppRunState(buf);
|
|
break;
|
|
case ENDPOINT_BLOBDB:
|
|
devEvts = new GBDeviceEvent[]{decodeBlobDb(buf)};
|
|
break;
|
|
case ENDPOINT_APPREORDER:
|
|
devEvts = new GBDeviceEvent[]{decodeAppReorder(buf)};
|
|
break;
|
|
case ENDPOINT_APPLOGS:
|
|
decodeAppLogs(buf);
|
|
break;
|
|
case ENDPOINT_VOICECONTROL:
|
|
devEvts = new GBDeviceEvent[]{decodeVoiceControl(buf)};
|
|
break;
|
|
case ENDPOINT_AUDIOSTREAM:
|
|
devEvts = new GBDeviceEvent[]{decodeAudioStream(buf)};
|
|
// LOG.debug("AUDIOSTREAM DATA: " + GB.hexdump(responseData, 4, length));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return devEvts;
|
|
}
|
|
|
|
void setForceProtocol(boolean force) {
|
|
LOG.info("setting force protocol to " + force);
|
|
mForceProtocol = force;
|
|
}
|
|
|
|
void setAlwaysACKPebbleKit(boolean alwaysACKPebbleKit) {
|
|
LOG.info("setting always ACK PebbleKit to " + alwaysACKPebbleKit);
|
|
mAlwaysACKPebbleKit = alwaysACKPebbleKit;
|
|
}
|
|
|
|
void setEnablePebbleKit(boolean enablePebbleKit) {
|
|
LOG.info("setting enable PebbleKit support to " + enablePebbleKit);
|
|
mEnablePebbleKit = enablePebbleKit;
|
|
}
|
|
|
|
boolean hasAppMessageHandler(UUID uuid) {
|
|
return mAppMessageHandlers.containsKey(uuid);
|
|
}
|
|
|
|
private String getFixedString(ByteBuffer buf, int length) {
|
|
byte[] tmp = new byte[length];
|
|
buf.get(tmp, 0, length);
|
|
|
|
return new String(tmp).trim();
|
|
}
|
|
|
|
private UUID getUUID(ByteBuffer buf) {
|
|
ByteOrder byteOrder = buf.order();
|
|
buf.order(ByteOrder.BIG_ENDIAN);
|
|
long uuid_high = buf.getLong();
|
|
long uuid_low = buf.getLong();
|
|
buf.order(byteOrder);
|
|
return new UUID(uuid_high, uuid_low);
|
|
}
|
|
}
|