diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/BluetoothCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/BluetoothCommunicationService.java index 1040266fa..3dde0bba5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/BluetoothCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/BluetoothCommunicationService.java @@ -1,48 +1,26 @@ package nodomain.freeyourgadget.gadgetbridge; -import android.app.Notification; +import nodomain.freeyourgadget.gadgetbridge.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.pebble.PebbleIoThread; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.protocol.MibandProtocol; +import nodomain.freeyourgadget.gadgetbridge.protocol.PebbleProtocol; import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothSocket; -import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.IBinder; -import android.os.ParcelUuid; import android.preference.PreferenceManager; import android.provider.ContactsContract; -import android.support.v4.app.NotificationCompat; -import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.widget.Toast; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.zip.ZipInputStream; - -import nodomain.freeyourgadget.gadgetbridge.pebble.PBWReader; -import nodomain.freeyourgadget.gadgetbridge.pebble.STM32CRC; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommand; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandAppInfo; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandAppManagementResult; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandCallControl; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandMusicControl; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandVersionInfo; -import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceProtocol; -import nodomain.freeyourgadget.gadgetbridge.protocol.MibandProtocol; -import nodomain.freeyourgadget.gadgetbridge.protocol.PebbleProtocol; - public class BluetoothCommunicationService extends Service { public static final String ACTION_START = "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.start"; @@ -70,9 +48,7 @@ public class BluetoothCommunicationService extends Service { = "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.install_pebbbleapp"; private static final String TAG = "CommunicationService"; - private static final int NOTIFICATION_ID = 1; private BluetoothAdapter mBtAdapter = null; - private BluetoothSocket mBtSocket = null; private GBDeviceIoThread mGBDeviceIoThread = null; private boolean mStarted = false; @@ -80,165 +56,11 @@ public class BluetoothCommunicationService extends Service { private GBDevice mGBDevice = null; private GBDeviceProtocol mGBDeviceProtocol = null; - private void setReceiversEnableState(boolean enable) { - final Class[] receiverClasses = { - PhoneCallReceiver.class, - SMSReceiver.class, - K9Receiver.class, - MusicPlaybackReceiver.class, - //NotificationListener.class, // disabling this leads to loss of permission to read notifications - }; - - int newState; - - if (enable) { - newState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED; - } else { - newState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - } - - PackageManager pm = getPackageManager(); - - for (Class receiverClass : receiverClasses) { - ComponentName compName = new ComponentName(getApplicationContext(), receiverClass); - - pm.setComponentEnabledSetting(compName, newState, PackageManager.DONT_KILL_APP); - } - } - @Override public void onCreate() { super.onCreate(); } - - private Notification createNotification(String text) { - Intent notificationIntent = new Intent(this, ControlCenter.class); - notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TASK); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, - notificationIntent, 0); - - return new NotificationCompat.Builder(this) - .setContentTitle("Gadgetbridge") - .setTicker(text) - .setContentText(text) - .setSmallIcon(R.drawable.ic_notification) - .setContentIntent(pendingIntent) - .setOngoing(true).build(); - } - - private void updateNotification(String text) { - - Notification notification = createNotification(text); - - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - - private void evaluateGBCommandBundle(GBDeviceCommand deviceCmd) { - switch (deviceCmd.commandClass) { - case MUSIC_CONTROL: - Log.i(TAG, "Got command for MUSIC_CONTROL"); - GBDeviceCommandMusicControl musicCmd = (GBDeviceCommandMusicControl) deviceCmd; - Intent musicIntent = new Intent(GBMusicControlReceiver.ACTION_MUSICCONTROL); - musicIntent.putExtra("command", musicCmd.command.ordinal()); - musicIntent.setPackage(this.getPackageName()); - sendBroadcast(musicIntent); - break; - case CALL_CONTROL: - Log.i(TAG, "Got command for CALL_CONTROL"); - GBDeviceCommandCallControl callCmd = (GBDeviceCommandCallControl) deviceCmd; - Intent callIntent = new Intent(GBCallControlReceiver.ACTION_CALLCONTROL); - callIntent.putExtra("command", callCmd.command.ordinal()); - callIntent.setPackage(this.getPackageName()); - sendBroadcast(callIntent); - break; - case VERSION_INFO: - Log.i(TAG, "Got command for VERSION_INFO"); - if (mGBDevice == null) { - return; - } - GBDeviceCommandVersionInfo infoCmd = (GBDeviceCommandVersionInfo) deviceCmd; - mGBDevice.setFirmwareVersion(infoCmd.fwVersion); - sendDeviceUpdateIntent(); - break; - case APP_INFO: - Log.i(TAG, "Got command for APP_INFO"); - GBDeviceCommandAppInfo appInfoCmd = (GBDeviceCommandAppInfo) deviceCmd; - if (mGBDevice.getType() == GBDevice.Type.PEBBLE) { - ((PebbleIoThread) mGBDeviceIoThread).setInstallSlot(appInfoCmd.freeSlot); - } - - Intent appInfoIntent = new Intent(AppManagerActivity.ACTION_REFRESH_APPLIST); - int appCount = appInfoCmd.apps.length; - appInfoIntent.putExtra("app_count", appCount); - for (Integer i = 0; i < appCount; i++) { - appInfoIntent.putExtra("app_name" + i.toString(), appInfoCmd.apps[i].getName()); - appInfoIntent.putExtra("app_creator" + i.toString(), appInfoCmd.apps[i].getCreator()); - appInfoIntent.putExtra("app_id" + i.toString(), appInfoCmd.apps[i].getId()); - appInfoIntent.putExtra("app_index" + i.toString(), appInfoCmd.apps[i].getIndex()); - appInfoIntent.putExtra("app_type" + i.toString(), appInfoCmd.apps[i].getType().ordinal()); - } - LocalBroadcastManager.getInstance(this).sendBroadcast(appInfoIntent); - break; - case APP_MANAGEMENT_RES: - GBDeviceCommandAppManagementResult appMgmtRes = (GBDeviceCommandAppManagementResult) deviceCmd; - switch (appMgmtRes.type) { - case DELETE: - // right now on the Pebble we also receive this on a failed/successful installation ;/ - switch (appMgmtRes.result) { - case FAILURE: - Log.i(TAG, "failure removing app"); // TODO: report to AppManager - if (mGBDevice.getType() == GBDevice.Type.PEBBLE) { - ((PebbleIoThread) mGBDeviceIoThread).finishInstall(true); - } - break; - case SUCCESS: - if (mGBDevice.getType() == GBDevice.Type.PEBBLE) { - ((PebbleIoThread) mGBDeviceIoThread).finishInstall(false); - } - // refresh app list - mGBDeviceIoThread.write(mGBDeviceProtocol.encodeAppInfoReq()); - break; - default: - break; - } - break; - case INSTALL: - switch (appMgmtRes.result) { - case FAILURE: - Log.i(TAG, "failure installing app"); // TODO: report to Installer - if (mGBDevice.getType() == GBDevice.Type.PEBBLE) { - ((PebbleIoThread) mGBDeviceIoThread).finishInstall(true); - } - break; - case SUCCESS: - if (mGBDevice.getType() == GBDevice.Type.PEBBLE) { - ((PebbleIoThread) mGBDeviceIoThread).setToken(appMgmtRes.token); - } - break; - default: - break; - } - break; - default: - break; - } - default: - break; - } - } - - private void sendDeviceUpdateIntent() { - Intent deviceUpdateIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST); - deviceUpdateIntent.putExtra("device_address", mGBDevice.getAddress()); - deviceUpdateIntent.putExtra("device_state", mGBDevice.getState().ordinal()); - deviceUpdateIntent.putExtra("firmware_version", mGBDevice.getFirmwareVersion()); - - LocalBroadcastManager.getInstance(this).sendBroadcast(deviceUpdateIntent); - } - @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -264,8 +86,8 @@ public class BluetoothCommunicationService extends Service { return START_STICKY; } - if (!action.equals(ACTION_START) && !action.equals(ACTION_CONNECT) && mBtSocket == null) { - // trying to send notification without valid Blutooth socket + if (!action.equals(ACTION_START) && !action.equals(ACTION_CONNECT) && !isConnected()) { + // trying to send notification without valid Blutooth connection return START_STICKY; } @@ -281,7 +103,7 @@ public class BluetoothCommunicationService extends Service { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); sharedPrefs.edit().putString("last_device_address", btDeviceAddress).commit(); - if (btDeviceAddress != null && (mBtSocket == null || !mBtSocket.isConnected())) { + if (btDeviceAddress != null && !isConnected()) { // currently only one thread allowed if (mGBDeviceIoThread != null) { mGBDeviceIoThread.quit(); @@ -294,20 +116,18 @@ public class BluetoothCommunicationService extends Service { } BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress); if (btDevice != null) { - GBDevice.Type deviceType = GBDevice.Type.UNKNOWN; if (btDevice.getName() == null || btDevice.getName().equals("MI")) { //FIXME: workaround for Miband not being paired - deviceType = GBDevice.Type.MIBAND; + mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), GBDevice.Type.MIBAND); mGBDeviceProtocol = new MibandProtocol(); - mGBDeviceIoThread = new MibandIoThread(btDeviceAddress); + mGBDeviceIoThread = new MibandIoThread(mGBDevice, this); } else if (btDevice.getName().indexOf("Pebble") == 0) { - deviceType = GBDevice.Type.PEBBLE; + mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), GBDevice.Type.PEBBLE); mGBDeviceProtocol = new PebbleProtocol(); - mGBDeviceIoThread = new PebbleIoThread(btDeviceAddress); + mGBDeviceIoThread = new PebbleIoThread(mGBDevice, mGBDeviceProtocol, mBtAdapter, this); } if (mGBDeviceProtocol != null) { - mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), deviceType); mGBDevice.setState(GBDevice.State.CONNECTING); - sendDeviceUpdateIntent(); + mGBDevice.sendDeviceUpdateIntent(this); mGBDeviceIoThread.start(); } @@ -354,7 +174,7 @@ public class BluetoothCommunicationService extends Service { byte[] msg = mGBDeviceProtocol.encodeFirmwareVersionReq(); mGBDeviceIoThread.write(msg); } else { - sendDeviceUpdateIntent(); + mGBDevice.sendDeviceUpdateIntent(this); } } else if (action.equals(ACTION_REQUEST_APPINFO)) { mGBDeviceIoThread.write(mGBDeviceProtocol.encodeAppInfoReq()); @@ -369,18 +189,22 @@ public class BluetoothCommunicationService extends Service { ((PebbleIoThread) mGBDeviceIoThread).installApp(Uri.parse(uriString)); } } else if (action.equals(ACTION_START)) { - startForeground(NOTIFICATION_ID, createNotification("Gadgetbridge running")); + startForeground(GB.NOTIFICATION_ID, GB.createNotification("Gadgetbridge running", this)); mStarted = true; } return START_STICKY; } - @Override + private boolean isConnected() { + return mGBDevice != null && mGBDevice.getState() == State.CONNECTED; + } + + @Override public void onDestroy() { super.onDestroy(); - setReceiversEnableState(false); // disable BroadcastReceivers + GB.setReceiversEnableState(false, this); // disable BroadcastReceivers if (mGBDeviceIoThread != null) { try { @@ -391,7 +215,7 @@ public class BluetoothCommunicationService extends Service { } } NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops + nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops } @Override @@ -424,362 +248,4 @@ public class BluetoothCommunicationService extends Service { return name; } - - private abstract class GBDeviceIoThread extends Thread { - protected final String mmBtDeviceAddress; - - public GBDeviceIoThread(String btDeviceAddress) { - mmBtDeviceAddress = btDeviceAddress; - } - - private boolean connect(String btDeviceAddress) { - return false; - } - - public void run() { - } - - synchronized public void write(byte[] bytes) { - } - - public void quit() { - } - } - - private class MibandIoThread extends GBDeviceIoThread { - public MibandIoThread(String btDeviceAddress) { - super(btDeviceAddress); - } - - // implement connect() run() write() and quit() here - } - - private enum PebbleAppInstallState { - UNKNOWN, - APP_WAIT_SLOT, - APP_START_INSTALL, - APP_WAIT_TOKEN, - APP_UPLOAD_CHUNK, - APP_UPLOAD_COMMIT, - APP_WAIT_COMMMIT, - APP_UPLOAD_COMPLETE, - APP_REFRESH, - } - - private class PebbleIoThread extends GBDeviceIoThread { - private final PebbleProtocol mmPebbleProtocol; - private InputStream mmInStream = null; - private OutputStream mmOutStream = null; - private boolean mmQuit = false; - private boolean mmIsConnected = false; - private boolean mmIsInstalling = false; - private int mmConnectionAttempts = 0; - - /* app installation */ - private Uri mmInstallURI = null; - private PBWReader mmPBWReader = null; - private int mmAppInstallToken = -1; - private ZipInputStream mmZis = null; - private STM32CRC mmSTM32CRC = new STM32CRC(); - private PebbleAppInstallState mmInstallState = PebbleAppInstallState.UNKNOWN; - private String[] mmFilesToInstall = null; - private int mmCurrentFileIndex = -1; - private int mmInstallSlot = -1; - - public PebbleIoThread(String btDeviceAddress) { - super(btDeviceAddress); - mmPebbleProtocol = (PebbleProtocol) mGBDeviceProtocol; - } - - private boolean connect(String btDeviceAddress) { - BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress); - ParcelUuid uuids[] = btDevice.getUuids(); - try { - mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); - mBtSocket.connect(); - mmInStream = mBtSocket.getInputStream(); - mmOutStream = mBtSocket.getOutputStream(); - } catch (IOException e) { - e.printStackTrace(); - mmInStream = null; - mmOutStream = null; - mBtSocket = null; - return false; - } - mGBDevice.setState(GBDevice.State.CONNECTED); - sendDeviceUpdateIntent(); - updateNotification("connected to " + btDevice.getName()); - - return true; - } - - public void run() { - mmIsConnected = connect(mmBtDeviceAddress); - setReceiversEnableState(mmIsConnected); // enable/disable BroadcastReceivers - mmQuit = !mmIsConnected; // quit if not connected - - byte[] buffer = new byte[8192]; - int bytes; - - while (!mmQuit) { - try { - if (mmIsInstalling) { - switch (mmInstallState) { - case APP_WAIT_SLOT: - if (mmInstallSlot != -1) { - updateNotification("starting installation"); - mmInstallState = PebbleAppInstallState.APP_START_INSTALL; - continue; - } - break; - case APP_START_INSTALL: - Log.i(TAG, "start installing app binary"); - mmSTM32CRC.reset(); - if (mmPBWReader == null) { - mmPBWReader = new PBWReader(mmInstallURI, getApplicationContext()); - mmFilesToInstall = mmPBWReader.getFilesToInstall(); - mmCurrentFileIndex = 0; - } - String fileName = mmFilesToInstall[mmCurrentFileIndex]; - mmZis = mmPBWReader.getInputStreamFile(fileName); - int binarySize = mmPBWReader.getFileSize(fileName); - // FIXME: do not assume type from filename, parse json correctly in PBWReader - byte type = -1; - if (fileName.equals("pebble-app.bin")) { - type = PebbleProtocol.PUTBYTES_TYPE_BINARY; - } else if (fileName.equals("pebble-worker.bin")) { - type = PebbleProtocol.PUTBYTES_TYPE_WORKER; - } else if (fileName.equals("app_resources.pbpack")) { - type = PebbleProtocol.PUTBYTES_TYPE_RESOURCES; - } else { - finishInstall(true); - break; - } - - writeInstallApp(mmPebbleProtocol.encodeUploadStart(type, (byte) mmInstallSlot, binarySize)); - mmInstallState = PebbleAppInstallState.APP_WAIT_TOKEN; - break; - case APP_WAIT_TOKEN: - if (mmAppInstallToken != -1) { - Log.i(TAG, "got token " + mmAppInstallToken); - mmInstallState = PebbleAppInstallState.APP_UPLOAD_CHUNK; - continue; - } - break; - case APP_UPLOAD_CHUNK: - bytes = mmZis.read(buffer); - - if (bytes != -1) { - mmSTM32CRC.addData(buffer, bytes); - writeInstallApp(mmPebbleProtocol.encodeUploadChunk(mmAppInstallToken, buffer, bytes)); - mmAppInstallToken = -1; - mmInstallState = PebbleAppInstallState.APP_WAIT_TOKEN; - } else { - mmInstallState = PebbleAppInstallState.APP_UPLOAD_COMMIT; - continue; - } - break; - case APP_UPLOAD_COMMIT: - writeInstallApp(mmPebbleProtocol.encodeUploadCommit(mmAppInstallToken, mmSTM32CRC.getResult())); - mmAppInstallToken = -1; - mmInstallState = PebbleAppInstallState.APP_WAIT_COMMMIT; - break; - case APP_WAIT_COMMMIT: - if (mmAppInstallToken != -1) { - Log.i(TAG, "got token " + mmAppInstallToken); - mmInstallState = PebbleAppInstallState.APP_UPLOAD_COMPLETE; - continue; - } - break; - case APP_UPLOAD_COMPLETE: - writeInstallApp(mmPebbleProtocol.encodeUploadComplete(mmAppInstallToken)); - if (++mmCurrentFileIndex < mmFilesToInstall.length) { - mmInstallState = PebbleAppInstallState.APP_START_INSTALL; - } else { - mmInstallState = PebbleAppInstallState.APP_REFRESH; - } - break; - case APP_REFRESH: - writeInstallApp(mmPebbleProtocol.encodeAppRefresh(mmInstallSlot)); - break; - default: - break; - } - } - bytes = mmInStream.read(buffer, 0, 4); - if (bytes < 4) - continue; - - ByteBuffer buf = ByteBuffer.wrap(buffer); - buf.order(ByteOrder.BIG_ENDIAN); - short length = buf.getShort(); - short endpoint = buf.getShort(); - if (length < 0 || length > 8192) { - Log.i(TAG, "invalid length " + length); - while (mmInStream.available() > 0) { - mmInStream.read(buffer); // read all - } - continue; - } - - bytes = mmInStream.read(buffer, 4, length); - if (bytes < length) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - Log.i(TAG, "Read " + bytes + ", expected " + length + " reading remaining " + (length - bytes)); - int bytes_rest = mmInStream.read(buffer, 4 + bytes, length - bytes); - bytes += bytes_rest; - } - - if (length == 1 && endpoint == PebbleProtocol.ENDPOINT_PHONEVERSION) { - Log.i(TAG, "Pebble asked for Phone/App Version - repLYING!"); - write(mmPebbleProtocol.encodePhoneVersion(PebbleProtocol.PHONEVERSION_REMOTE_OS_ANDROID)); - write(mmPebbleProtocol.encodeFirmwareVersionReq()); - - // this does not really belong here, but since the pebble only asks for our version once it should do the job - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(BluetoothCommunicationService.this); - if (sharedPrefs.getBoolean("datetime_synconconnect", true)) { - Log.i(TAG, "syncing time"); - write(mmPebbleProtocol.encodeSetTime(-1)); - } - } else if (endpoint != PebbleProtocol.ENDPOINT_DATALOG) { - GBDeviceCommand deviceCmd = mmPebbleProtocol.decodeResponse(buffer); - if (deviceCmd == null) { - Log.i(TAG, "unhandled message to endpoint " + endpoint + " (" + bytes + " bytes)"); - } else { - evaluateGBCommandBundle(deviceCmd); - } - } - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } catch (IOException e) { - if (e.getMessage().contains("socket closed")) { //FIXME: this does not feel right - Log.i(TAG, e.getMessage()); - mGBDevice.setState(GBDevice.State.CONNECTING); - sendDeviceUpdateIntent(); - updateNotification("connection lost, trying to reconnect"); - - while (mmConnectionAttempts++ < 10) { - Log.i(TAG, "Trying to reconnect (attempt " + mmConnectionAttempts + ")"); - mmIsConnected = connect(mmBtDeviceAddress); - if (mmIsConnected) - break; - } - mmConnectionAttempts = 0; - if (!mmIsConnected) { - mBtSocket = null; - setReceiversEnableState(false); - Log.i(TAG, "Bluetooth socket closed, will quit IO Thread"); - mmQuit = true; - } - } - } - } - mmIsConnected = false; - if (mBtSocket != null) { - try { - mBtSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - mBtSocket = null; - updateNotification("not connected"); - mGBDevice.setState(GBDevice.State.NOT_CONNECTED); - sendDeviceUpdateIntent(); - } - - synchronized public void write(byte[] bytes) { - // block writes if app installation in in progress - if (mmIsConnected && !mmIsInstalling) { - try { - mmOutStream.write(bytes); - mmOutStream.flush(); - } catch (IOException e) { - } - } - } - - public void setToken(int token) { - mmAppInstallToken = token; - } - - public void setInstallSlot(int slot) { - if (mmIsInstalling) { - mmInstallSlot = slot; - } - } - - private void writeInstallApp(byte[] bytes) { - if (!mmIsInstalling) { - return; - } - int length = bytes.length; - Log.i(TAG, "got bytes for writeInstallApp()" + length); -/* - final char[] hexArray = "0123456789ABCDEF".toCharArray(); - char[] hexChars = new char[length * 2]; - for (int j = 0; j < length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - Log.i(TAG, new String(hexChars)); -*/ - try { - mmOutStream.write(bytes); - mmOutStream.flush(); - } catch (IOException e) { - } - } - - public void installApp(Uri uri) { - if (mmIsInstalling) { - return; - } - write(mmPebbleProtocol.encodeAppInfoReq()); // do this here to get run() out of its blocking read - mmInstallState = PebbleAppInstallState.APP_WAIT_SLOT; - mmInstallURI = uri; - mmIsInstalling = true; - } - - public void finishInstall(boolean hadError) { - if (!mmIsInstalling) { - return; - } - if (hadError) { - updateNotification("installation failed!"); - } else { - updateNotification("installation successful"); - } - mmInstallState = PebbleAppInstallState.UNKNOWN; - - if (hadError == true && mmAppInstallToken != -1) { - writeInstallApp(mmPebbleProtocol.encodeUploadCancel(mmAppInstallToken)); - } - - mmPBWReader = null; - mmIsInstalling = false; - mmZis = null; - mmAppInstallToken = -1; - mmInstallSlot = -1; - } - - public void quit() { - mmQuit = true; - if (mBtSocket != null) { - try { - mBtSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GB.java new file mode 100644 index 000000000..4fc8a1535 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GB.java @@ -0,0 +1,63 @@ +package nodomain.freeyourgadget.gadgetbridge; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.support.v4.app.NotificationCompat; + +public class GB { + public static final int NOTIFICATION_ID = 1; + + public static Notification createNotification(String text, Context context) { + Intent notificationIntent = new Intent(context, ControlCenter.class); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, + notificationIntent, 0); + + return new NotificationCompat.Builder(context) + .setContentTitle("Gadgetbridge") + .setTicker(text) + .setContentText(text) + .setSmallIcon(R.drawable.ic_notification) + .setContentIntent(pendingIntent) + .setOngoing(true).build(); + } + + public static void updateNotification(String text, Context context) { + Notification notification = createNotification(text, context); + + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + + public static void setReceiversEnableState(boolean enable, Context context) { + final Class[] receiverClasses = { + PhoneCallReceiver.class, + SMSReceiver.class, + K9Receiver.class, + MusicPlaybackReceiver.class, + //NotificationListener.class, // disabling this leads to loss of permission to read notifications + }; + + int newState; + + if (enable) { + newState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + } else { + newState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } + + PackageManager pm = context.getPackageManager(); + + for (Class receiverClass : receiverClasses) { + ComponentName compName = new ComponentName(context, receiverClass); + + pm.setComponentEnabledSetting(compName, newState, PackageManager.DONT_KILL_APP); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDevice.java index fb7d22e8a..039f64128 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDevice.java @@ -1,5 +1,9 @@ package nodomain.freeyourgadget.gadgetbridge; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + public class GBDevice { private final String name; private final String address; @@ -61,6 +65,15 @@ public class GBDevice { return type; } + public void sendDeviceUpdateIntent(Context context) { + Intent deviceUpdateIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST); + deviceUpdateIntent.putExtra("device_address", getAddress()); + deviceUpdateIntent.putExtra("device_state", getState().ordinal()); + deviceUpdateIntent.putExtra("firmware_version", getFirmwareVersion()); + + LocalBroadcastManager.getInstance(context).sendBroadcast(deviceUpdateIntent); + } + public enum State { NOT_CONNECTED, CONNECTING, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDeviceIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDeviceIoThread.java new file mode 100644 index 000000000..1256c3ade --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBDeviceIoThread.java @@ -0,0 +1,34 @@ +package nodomain.freeyourgadget.gadgetbridge; + +import android.content.Context; + +public abstract class GBDeviceIoThread extends Thread { + protected final GBDevice gbDevice; + private final Context context; + + public GBDeviceIoThread(GBDevice gbDevice, Context context) { + this.gbDevice = gbDevice; + this.context = context; + } + + public Context getContext() { + return context; + } + + public GBDevice getDevice() { + return gbDevice; + } + + protected boolean connect(String btDeviceAddress) { + return false; + } + + public void run() { + } + + synchronized public void write(byte[] bytes) { + } + + public void quit() { + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/MibandIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/MibandIoThread.java new file mode 100644 index 000000000..a61940275 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/MibandIoThread.java @@ -0,0 +1,11 @@ +package nodomain.freeyourgadget.gadgetbridge; + +import android.content.Context; + +class MibandIoThread extends GBDeviceIoThread { + public MibandIoThread(GBDevice gbDevice, Context context) { + super(gbDevice, context); + } + + // implement connect() run() write() and quit() here +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleIoThread.java new file mode 100644 index 000000000..cd3caacea --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleIoThread.java @@ -0,0 +1,455 @@ +package nodomain.freeyourgadget.gadgetbridge.pebble; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.ZipInputStream; + +import nodomain.freeyourgadget.gadgetbridge.AppManagerActivity; +import nodomain.freeyourgadget.gadgetbridge.GB; +import nodomain.freeyourgadget.gadgetbridge.GBCallControlReceiver; +import nodomain.freeyourgadget.gadgetbridge.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.GBMusicControlReceiver; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommand; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandAppInfo; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandAppManagementResult; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandCallControl; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandMusicControl; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.protocol.PebbleProtocol; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.ParcelUuid; +import android.preference.PreferenceManager; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +public class PebbleIoThread extends GBDeviceIoThread { + private static final String TAG = PebbleIoThread.class.getSimpleName(); + + private enum PebbleAppInstallState { + UNKNOWN, + APP_WAIT_SLOT, + APP_START_INSTALL, + APP_WAIT_TOKEN, + APP_UPLOAD_CHUNK, + APP_UPLOAD_COMMIT, + APP_WAIT_COMMMIT, + APP_UPLOAD_COMPLETE, + APP_REFRESH, + } + + private final PebbleProtocol mmPebbleProtocol; + private InputStream mmInStream = null; + private OutputStream mmOutStream = null; + private boolean mmQuit = false; + private boolean mmIsConnected = false; + private boolean mmIsInstalling = false; + private int mmConnectionAttempts = 0; + + /* app installation */ + private Uri mmInstallURI = null; + private PBWReader mmPBWReader = null; + private int mmAppInstallToken = -1; + private ZipInputStream mmZis = null; + private STM32CRC mmSTM32CRC = new STM32CRC(); + private PebbleAppInstallState mmInstallState = PebbleAppInstallState.UNKNOWN; + private String[] mmFilesToInstall = null; + private int mmCurrentFileIndex = -1; + private int mmInstallSlot = -1; + private BluetoothAdapter mBtAdapter = null; + private BluetoothSocket mBtSocket = null; + + public PebbleIoThread(GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) { + super(gbDevice, context); + mmPebbleProtocol = (PebbleProtocol) gbDeviceProtocol; + mBtAdapter = btAdapter; + } + + protected boolean connect(String btDeviceAddress) { + BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress); + ParcelUuid uuids[] = btDevice.getUuids(); + try { + mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); + mBtSocket.connect(); + mmInStream = mBtSocket.getInputStream(); + mmOutStream = mBtSocket.getOutputStream(); + } catch (IOException e) { + e.printStackTrace(); + gbDevice.setState(GBDevice.State.NOT_CONNECTED); + mmInStream = null; + mmOutStream = null; + mBtSocket = null; + return false; + } + gbDevice.setState(GBDevice.State.CONNECTED); + gbDevice.sendDeviceUpdateIntent(getContext()); + GB.updateNotification("connected to " + btDevice.getName(), getContext()); + + return true; + } + + public void run() { + mmIsConnected = connect(gbDevice.getAddress()); + GB.setReceiversEnableState(mmIsConnected, getContext()); // enable/disable BroadcastReceivers + mmQuit = !mmIsConnected; // quit if not connected + + byte[] buffer = new byte[8192]; + int bytes; + + while (!mmQuit) { + try { + if (mmIsInstalling) { + switch (mmInstallState) { + case APP_WAIT_SLOT: + if (mmInstallSlot != -1) { + GB.updateNotification("starting installation", getContext()); + mmInstallState = PebbleAppInstallState.APP_START_INSTALL; + continue; + } + break; + case APP_START_INSTALL: + Log.i(TAG, "start installing app binary"); + mmSTM32CRC.reset(); + if (mmPBWReader == null) { + mmPBWReader = new PBWReader(mmInstallURI, getContext()); + mmFilesToInstall = mmPBWReader.getFilesToInstall(); + mmCurrentFileIndex = 0; + } + String fileName = mmFilesToInstall[mmCurrentFileIndex]; + mmZis = mmPBWReader.getInputStreamFile(fileName); + int binarySize = mmPBWReader.getFileSize(fileName); + // FIXME: do not assume type from filename, parse json correctly in PBWReader + byte type = -1; + if (fileName.equals("pebble-app.bin")) { + type = PebbleProtocol.PUTBYTES_TYPE_BINARY; + } else if (fileName.equals("pebble-worker.bin")) { + type = PebbleProtocol.PUTBYTES_TYPE_WORKER; + } else if (fileName.equals("app_resources.pbpack")) { + type = PebbleProtocol.PUTBYTES_TYPE_RESOURCES; + } else { + finishInstall(true); + break; + } + + writeInstallApp(mmPebbleProtocol.encodeUploadStart(type, (byte) mmInstallSlot, binarySize)); + mmInstallState = PebbleAppInstallState.APP_WAIT_TOKEN; + break; + case APP_WAIT_TOKEN: + if (mmAppInstallToken != -1) { + Log.i(TAG, "got token " + mmAppInstallToken); + mmInstallState = PebbleAppInstallState.APP_UPLOAD_CHUNK; + continue; + } + break; + case APP_UPLOAD_CHUNK: + bytes = mmZis.read(buffer); + + if (bytes != -1) { + mmSTM32CRC.addData(buffer, bytes); + writeInstallApp(mmPebbleProtocol.encodeUploadChunk(mmAppInstallToken, buffer, bytes)); + mmAppInstallToken = -1; + mmInstallState = PebbleAppInstallState.APP_WAIT_TOKEN; + } else { + mmInstallState = PebbleAppInstallState.APP_UPLOAD_COMMIT; + continue; + } + break; + case APP_UPLOAD_COMMIT: + writeInstallApp(mmPebbleProtocol.encodeUploadCommit(mmAppInstallToken, mmSTM32CRC.getResult())); + mmAppInstallToken = -1; + mmInstallState = PebbleAppInstallState.APP_WAIT_COMMMIT; + break; + case APP_WAIT_COMMMIT: + if (mmAppInstallToken != -1) { + Log.i(TAG, "got token " + mmAppInstallToken); + mmInstallState = PebbleAppInstallState.APP_UPLOAD_COMPLETE; + continue; + } + break; + case APP_UPLOAD_COMPLETE: + writeInstallApp(mmPebbleProtocol.encodeUploadComplete(mmAppInstallToken)); + if (++mmCurrentFileIndex < mmFilesToInstall.length) { + mmInstallState = PebbleAppInstallState.APP_START_INSTALL; + } else { + mmInstallState = PebbleAppInstallState.APP_REFRESH; + } + break; + case APP_REFRESH: + writeInstallApp(mmPebbleProtocol.encodeAppRefresh(mmInstallSlot)); + break; + default: + break; + } + } + bytes = mmInStream.read(buffer, 0, 4); + if (bytes < 4) + continue; + + ByteBuffer buf = ByteBuffer.wrap(buffer); + buf.order(ByteOrder.BIG_ENDIAN); + short length = buf.getShort(); + short endpoint = buf.getShort(); + if (length < 0 || length > 8192) { + Log.i(TAG, "invalid length " + length); + while (mmInStream.available() > 0) { + mmInStream.read(buffer); // read all + } + continue; + } + + bytes = mmInStream.read(buffer, 4, length); + if (bytes < length) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Log.i(TAG, "Read " + bytes + ", expected " + length + " reading remaining " + (length - bytes)); + int bytes_rest = mmInStream.read(buffer, 4 + bytes, length - bytes); + bytes += bytes_rest; + } + + if (length == 1 && endpoint == PebbleProtocol.ENDPOINT_PHONEVERSION) { + Log.i(TAG, "Pebble asked for Phone/App Version - repLYING!"); + write(mmPebbleProtocol.encodePhoneVersion(PebbleProtocol.PHONEVERSION_REMOTE_OS_ANDROID)); + write(mmPebbleProtocol.encodeFirmwareVersionReq()); + + // this does not really belong here, but since the pebble only asks for our version once it should do the job + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (sharedPrefs.getBoolean("datetime_synconconnect", true)) { + Log.i(TAG, "syncing time"); + write(mmPebbleProtocol.encodeSetTime(-1)); + } + } else if (endpoint != PebbleProtocol.ENDPOINT_DATALOG) { + GBDeviceCommand deviceCmd = mmPebbleProtocol.decodeResponse(buffer); + if (deviceCmd == null) { + Log.i(TAG, "unhandled message to endpoint " + endpoint + " (" + bytes + " bytes)"); + } else { + evaluateGBCommandBundle(deviceCmd); + } + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (IOException e) { + if (e.getMessage().contains("socket closed")) { //FIXME: this does not feel right + Log.i(TAG, e.getMessage()); + gbDevice.setState(GBDevice.State.CONNECTING); + gbDevice.sendDeviceUpdateIntent(getContext()); + GB.updateNotification("connection lost, trying to reconnect", getContext()); + + while (mmConnectionAttempts++ < 10) { + Log.i(TAG, "Trying to reconnect (attempt " + mmConnectionAttempts + ")"); + mmIsConnected = connect(gbDevice.getAddress()); + if (mmIsConnected) + break; + } + mmConnectionAttempts = 0; + if (!mmIsConnected) { + mBtSocket = null; + GB.setReceiversEnableState(false, getContext()); + Log.i(TAG, "Bluetooth socket closed, will quit IO Thread"); + mmQuit = true; + } + } + } + } + mmIsConnected = false; + if (mBtSocket != null) { + try { + mBtSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + mBtSocket = null; + GB.updateNotification("not connected", getContext()); + gbDevice.setState(GBDevice.State.NOT_CONNECTED); + gbDevice.sendDeviceUpdateIntent(getContext()); + } + + synchronized public void write(byte[] bytes) { + // block writes if app installation in in progress + if (mmIsConnected && !mmIsInstalling) { + try { + mmOutStream.write(bytes); + mmOutStream.flush(); + } catch (IOException e) { + } + } + } + + private void evaluateGBCommandBundle(GBDeviceCommand deviceCmd) { + Context context = getContext(); + + switch (deviceCmd.commandClass) { + case MUSIC_CONTROL: + Log.i(TAG, "Got command for MUSIC_CONTROL"); + GBDeviceCommandMusicControl musicCmd = (GBDeviceCommandMusicControl) deviceCmd; + Intent musicIntent = new Intent(GBMusicControlReceiver.ACTION_MUSICCONTROL); + musicIntent.putExtra("command", musicCmd.command.ordinal()); + musicIntent.setPackage(context.getPackageName()); + context.sendBroadcast(musicIntent); + break; + case CALL_CONTROL: + Log.i(TAG, "Got command for CALL_CONTROL"); + GBDeviceCommandCallControl callCmd = (GBDeviceCommandCallControl) deviceCmd; + Intent callIntent = new Intent(GBCallControlReceiver.ACTION_CALLCONTROL); + callIntent.putExtra("command", callCmd.command.ordinal()); + callIntent.setPackage(context.getPackageName()); + context.sendBroadcast(callIntent); + break; + case VERSION_INFO: + Log.i(TAG, "Got command for VERSION_INFO"); + if (gbDevice == null) { + return; + } + GBDeviceCommandVersionInfo infoCmd = (GBDeviceCommandVersionInfo) deviceCmd; + gbDevice.setFirmwareVersion(infoCmd.fwVersion); + gbDevice.sendDeviceUpdateIntent(context); + break; + case APP_INFO: + Log.i(TAG, "Got command for APP_INFO"); + GBDeviceCommandAppInfo appInfoCmd = (GBDeviceCommandAppInfo) deviceCmd; + setInstallSlot(appInfoCmd.freeSlot); + + Intent appInfoIntent = new Intent(AppManagerActivity.ACTION_REFRESH_APPLIST); + int appCount = appInfoCmd.apps.length; + appInfoIntent.putExtra("app_count", appCount); + for (Integer i = 0; i < appCount; i++) { + appInfoIntent.putExtra("app_name" + i.toString(), appInfoCmd.apps[i].getName()); + appInfoIntent.putExtra("app_creator" + i.toString(), appInfoCmd.apps[i].getCreator()); + appInfoIntent.putExtra("app_id" + i.toString(), appInfoCmd.apps[i].getId()); + appInfoIntent.putExtra("app_index" + i.toString(), appInfoCmd.apps[i].getIndex()); + appInfoIntent.putExtra("app_type" + i.toString(), appInfoCmd.apps[i].getType().ordinal()); + } + LocalBroadcastManager.getInstance(context).sendBroadcast(appInfoIntent); + break; + case APP_MANAGEMENT_RES: + GBDeviceCommandAppManagementResult appMgmtRes = (GBDeviceCommandAppManagementResult) deviceCmd; + switch (appMgmtRes.type) { + case DELETE: + // right now on the Pebble we also receive this on a failed/successful installation ;/ + switch (appMgmtRes.result) { + case FAILURE: + Log.i(TAG, "failure removing app"); // TODO: report to AppManager + finishInstall(true); + break; + case SUCCESS: + finishInstall(false); + // refresh app list + write(mmPebbleProtocol.encodeAppInfoReq()); + break; + default: + break; + } + break; + case INSTALL: + switch (appMgmtRes.result) { + case FAILURE: + Log.i(TAG, "failure installing app"); // TODO: report to Installer + finishInstall(true); + break; + case SUCCESS: + setToken(appMgmtRes.token); + break; + default: + break; + } + break; + default: + break; + } + default: + break; + } + } + + public void setToken(int token) { + mmAppInstallToken = token; + } + + public void setInstallSlot(int slot) { + if (mmIsInstalling) { + mmInstallSlot = slot; + } + } + + private void writeInstallApp(byte[] bytes) { + if (!mmIsInstalling) { + return; + } + int length = bytes.length; + Log.i(TAG, "got bytes for writeInstallApp()" + length); + /* + final char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[length * 2]; + for (int j = 0; j < length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + Log.i(TAG, new String(hexChars)); + */ + try { + mmOutStream.write(bytes); + mmOutStream.flush(); + } catch (IOException e) { + } + } + + public void installApp(Uri uri) { + if (mmIsInstalling) { + return; + } + write(mmPebbleProtocol.encodeAppInfoReq()); // do this here to get run() out of its blocking read + mmInstallState = PebbleAppInstallState.APP_WAIT_SLOT; + mmInstallURI = uri; + mmIsInstalling = true; + } + + public void finishInstall(boolean hadError) { + if (!mmIsInstalling) { + return; + } + if (hadError) { + GB.updateNotification("installation failed!", getContext()); + } else { + GB.updateNotification("installation successful", getContext()); + } + mmInstallState = PebbleAppInstallState.UNKNOWN; + + if (hadError == true && mmAppInstallToken != -1) { + writeInstallApp(mmPebbleProtocol.encodeUploadCancel(mmAppInstallToken)); + } + + mmPBWReader = null; + mmIsInstalling = false; + mmZis = null; + mmAppInstallToken = -1; + mmInstallSlot = -1; + } + + public void quit() { + mmQuit = true; + if (mBtSocket != null) { + try { + mBtSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file