package nodomain.freeyourgadget.gadgetbridge; import android.util.Log; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.SimpleTimeZone; public class PebbleProtocol { static private String TAG = "PebbleProtocol"; static final short ENDPOINT_FIRMWARE = 1; static final short ENDPOINT_TIME = 11; static final short ENDPOINT_FIRMWAREVERSION = 16; static final short ENDPOINT_PHONEVERSION = 17; static final short ENDPOINT_SYSTEMMESSAGE = 18; static final short ENDPOINT_MUSICCONTROL = 32; static final short ENDPOINT_PHONECONTROL = 33; static final short ENDPOINT_APPLICATIONMESSAGE = 48; static final short ENDPOINT_LAUNCHER = 49; static final short ENDPOINT_LOGS = 2000; static final short ENDPOINT_PING = 2001; static final short ENDPOINT_LOGDUMP = 2002; static final short ENDPOINT_RESET = 2003; static final short ENDPOINT_APP = 2004; static final short ENDPOINT_APPLOGS = 2006; static final short ENDPOINT_NOTIFICATION = 3000; static final short ENDPOINT_RESOURCE = 4000; static final short ENDPOINT_SYSREG = 5000; static final short ENDPOINT_FCTREG = 5001; static final short ENDPOINT_APPMANAGER = 6000; static final short ENDPOINT_DATALOG = 6778; static final short ENDPOINT_RUNKEEPER = 7000; static final short ENDPOINT_SCREENSHOT = 8000; static final short ENDPOINT_PUTBYTES = (short) 48879; static final byte NOTIFICATION_EMAIL = 0; static final byte NOTIFICATION_SMS = 1; static final byte NOTIFICATION_TWITTER = 2; static final byte NOTIFICATION_FACEBOOK = 3; static final byte PHONECONTROL_ANSWER = 1; static final byte PHONECONTROL_HANGUP = 2; static final byte PHONECONTROL_GETSTATE = 3; static final byte PHONECONTROL_INCOMINGCALL = 4; static final byte PHONECONTROL_OUTGOINGCALL = 5; static final byte PHONECONTROL_MISSEDCALL = 6; static final byte PHONECONTROL_RING = 7; static final byte PHONECONTROL_START = 8; static final byte PHONECONTROL_END = 9; static final byte MUSICCONTROL_SETMUSICINFO = 16; static final byte MUSICCONTROL_PLAYPAUSE = 1; static final byte MUSICCONTROL_PAUSE = 2; static final byte MUSICCONTROL_PLAY = 3; static final byte MUSICCONTROL_NEXT = 4; static final byte MUSICCONTROL_PREVIOUS = 5; static final byte MUSICCONTROL_VOLUMEUP = 6; static final byte MUSICCONTROL_VOLUMEDOWN = 7; static final byte MUSICCONTROL_GETNOWPLAYING = 7; static final short LENGTH_PREFIX = 4; static final short LENGTH_SETTIME = 9; static final short LENGTH_PHONEVERSION = 21; static final byte TIME_GETTIME = 0; static final byte TIME_SETTIME = 2; static final byte PHONEVERSION_APPVERSION_MAGIC = 2; // increase this if pebble complains static final byte PHONEVERSION_APPVERSION_MAJOR = 2; static final byte PHONEVERSION_APPVERSION_MINOR = 3; static final byte PHONEVERSION_APPVERSION_PATCH = 0; static final int PHONEVERSION_SESSION_CAPS_GAMMARAY = (int) 0x80000000; static final int PHONEVERSION_REMOTE_CAPS_TELEPHONY = 0x00000010; static final int PHONEVERSION_REMOTE_CAPS_SMS = 0x00000020; static final int PHONEVERSION_REMOTE_CAPS_GPS = 0x00000040; static final int PHONEVERSION_REMOTE_CAPS_BTLE = 0x00000080; static final int PHONEVERSION_REMOTE_CAPS_REARCAMERA = 0x00000100; static final int PHONEVERSION_REMOTE_CAPS_ACCEL = 0x00000200; static final int PHONEVERSION_REMOTE_CAPS_GYRO = 0x00000400; static final int PHONEVERSION_REMOTE_CAPS_COMPASS = 0x00000800; static final byte PHONEVERSION_REMOTE_OS_UNKNOWN = 0; static final byte PHONEVERSION_REMOTE_OS_IOS = 1; static final byte PHONEVERSION_REMOTE_OS_ANDROID = 2; static final byte PHONEVERSION_REMOTE_OS_OSX = 3; static final byte PHONEVERSION_REMOTE_OS_LINUX = 4; static final byte PHONEVERSION_REMOTE_OS_WINDOWS = 5; static byte[] encodeMessage(short endpoint, byte type, int cookie, String[] parts) { // Calculate length first int length = LENGTH_PREFIX + 1; 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 for (String s : parts) { if (s == null || s.equals("")) { //buf.put((byte)0x01); 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(); } public static byte[] encodeSMS(String from, String body) { Long ts = System.currentTimeMillis() / 1000; ts += SimpleTimeZone.getDefault().getOffset(ts) / 1000; String tsstring = ts.toString(); // SIC String[] parts = {from, body, tsstring}; return encodeMessage(ENDPOINT_NOTIFICATION, NOTIFICATION_SMS, 0, parts); } public static byte[] encodeEmail(String from, String subject, String body) { Long ts = System.currentTimeMillis() / 1000; ts += SimpleTimeZone.getDefault().getOffset(ts) / 1000; String tsstring = ts.toString(); // SIC String[] parts = {from, body, tsstring, subject}; return encodeMessage(ENDPOINT_NOTIFICATION, NOTIFICATION_EMAIL, 0, parts); } public static byte[] encodeSetTime(long ts) { if (ts == -1) { ts = System.currentTimeMillis() / 1000; ts += SimpleTimeZone.getDefault().getOffset(ts) / 1000; } ByteBuffer buf = ByteBuffer.allocate(LENGTH_SETTIME); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort((short) (LENGTH_SETTIME - LENGTH_PREFIX)); buf.putShort(ENDPOINT_TIME); buf.put(TIME_SETTIME); buf.putInt((int) ts); return buf.array(); } public static byte[] encodeSetCallState(String number, String name, GBCommand command) { String[] parts = {number, name}; byte pebbleCmd; switch (command) { case CALL_START: pebbleCmd = PHONECONTROL_START; break; case CALL_END: pebbleCmd = PHONECONTROL_END; break; case CALL_INCOMING: pebbleCmd = PHONECONTROL_INCOMINGCALL; break; case 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 static byte[] encodeSetMusicInfo(String artist, String album, String track) { String[] parts = {artist, album, track}; return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts); } public static byte[] encodePhoneVersion(byte os) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PHONEVERSION); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort((short) (LENGTH_PHONEVERSION - LENGTH_PREFIX)); 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(); } public static GBCommandBundle decodeResponse(byte[] responseData) { ByteBuffer buf = ByteBuffer.wrap(responseData); buf.order(ByteOrder.BIG_ENDIAN); short length = buf.getShort(); short endpoint = buf.getShort(); byte pebbleCmd = buf.get(); GBCommandBundle cmd = new GBCommandBundle(); switch (endpoint) { case ENDPOINT_MUSICCONTROL: cmd.commandClass = GBCommandClass.MUSIC_CONTROL; switch (pebbleCmd) { case MUSICCONTROL_NEXT: cmd.command = GBCommand.MUSIC_NEXT; break; case MUSICCONTROL_PREVIOUS: cmd.command = GBCommand.MUSIC_PREVIOUS; break; case MUSICCONTROL_PLAY: cmd.command = GBCommand.MUSIC_PLAY; break; case MUSICCONTROL_PAUSE: cmd.command = GBCommand.MUSIC_PAUSE; break; case MUSICCONTROL_PLAYPAUSE: cmd.command = GBCommand.MUSIC_PLAYPAUSE; break; default: cmd.command = GBCommand.UNDEFINEND; break; } break; case ENDPOINT_PHONECONTROL: cmd.commandClass = GBCommandClass.CALL_CONTROL; switch (pebbleCmd) { case PHONECONTROL_HANGUP: cmd.command = GBCommand.CALL_END; break; default: Log.i(TAG, "Unknown PHONECONTROL command" + pebbleCmd); cmd.command = GBCommand.UNDEFINEND; break; } break; default: return null; } return cmd; } }