package org.runnerup.hr; import android.annotation.TargetApi; import android.app.Activity; import android.os.Build; import android.os.Handler; import android.os.Looper; /** * Created by jonas on 11/9/14. * * The following class handles transparent retries for flaky bluetooth * The following features are implemented: * - connect() * if connect fails, it's retried N times * if connect takes longer than X it's retried * * - connected * if loosing connection when connected, * it will auto connect */ @TargetApi(Build.VERSION_CODES.FROYO) public class RetryingHRProviderProxy implements HRProvider, HRProvider.HRClient { private HRDeviceRef connectRef; enum State { OPENING, OPENED, SCANNING, CONNECTING, CONNECTED, DISCONNECTING, CLOSING, CLOSED, ERROR, RECONNECTING } private int attempt = 0; private final int kMaxConnectRetries = 3; private final int kMaxReconnectRetires = 10; private final HRProvider provider; private HRClient client = null; private Handler handler = null; private State state = State.CLOSED; private State requestedState = State.CLOSED; private int getMaxRetries() { switch(state) { case OPENING: case OPENED: case SCANNING: case CONNECTED: case DISCONNECTING: case CLOSING: case CLOSED: case ERROR: return 0; case CONNECTING: return kMaxConnectRetries; case RECONNECTING: return kMaxReconnectRetires; } return 0; } private boolean checkMaxAttempts() { attempt++; if (attempt > getMaxRetries()) return false; return true; } private int getRetryDelayMillis() { switch(state) { case OPENING: case OPENED: case SCANNING: case CONNECTED: case DISCONNECTING: case CLOSING: case CLOSED: case ERROR: return 0; case CONNECTING: return 750 * (attempt - 1); case RECONNECTING: return 3000 * (attempt < 6 ? attempt : 6); } return 0; } private void resetAttempts() { attempt = 0; } public RetryingHRProviderProxy(HRProvider src) { this.provider = src; } @Override public String getName() { return provider.getName(); } @Override public String getProviderName() { return provider.getProviderName(); } @Override public boolean isEnabled() { return provider.isEnabled(); } @Override public boolean startEnableIntent(Activity activity, int requestCode) { return provider.startEnableIntent(activity, requestCode); } @Override public void open(Handler handler, HRClient hrClient) { this.client = hrClient; this.handler = handler; this.requestedState = State.OPENED; state = State.OPENING; provider.open(handler, this); } @Override public void onOpenResult(boolean ok) { log("onOpenResult(" + ok + ")"); if (requestedState != State.OPENED) { /* ignore onOpenResult in weird state */ return; } if (ok == true) { state = State.OPENED; client.onOpenResult(ok); } else { state = State.CLOSED; client.onOpenResult(ok); } } @Override public void close() { state = State.CLOSED; requestedState = State.CLOSED; if (provider != null) { provider.stopScan(); provider.disconnect(); provider.close(); } client = null; } @Override public boolean isBondingDevice() { return provider.isBondingDevice(); } @Override public boolean isScanning() { return provider.isScanning(); } @Override public boolean isConnected() { return provider.isConnected(); } @Override public boolean isConnecting() { return (requestedState == State.CONNECTING); } @Override public void startScan() { this.state = State.SCANNING; this.requestedState = State.SCANNING; provider.startScan(); } @Override public void onScanResult(HRDeviceRef device) { client.onScanResult(device); } @Override public void stopScan() { this.state = State.OPENED; this.requestedState = State.OPENED; provider.stopScan(); } @Override public void connect(HRDeviceRef ref) { log("connect("+ref+")"); resetAttempts(); this.state = State.CONNECTING; this.requestedState = State.CONNECTED; this.connectRef = ref; provider.connect(ref); } @Override public void onConnectResult(boolean connectOK) { log("onConnectResult("+connectOK+")"); switch (requestedState) { case OPENING: case OPENED: case SCANNING: case CONNECTING: case CLOSING: case CLOSED: case ERROR: /* weird => ignore */ return; case CONNECTED: break; case DISCONNECTING: /* ignore */ return; } if (connectOK) { boolean reconnect = state == State.RECONNECTING; state = State.CONNECTED; requestedState = State.CONNECTED; if (!reconnect) { log("client.onConnectResult(true)"); client.onConnectResult(true); } return; } else { if (!checkMaxAttempts()) { state = State.OPENED; requestedState = State.OPENED; log("client.onConnectResult(false)"); client.onConnectResult(false); return; } int delayMillis = getRetryDelayMillis(); log("retry in " + delayMillis + "ms"); handler.postDelayed(new Runnable() { @Override public void run() { log("retry connect"); provider.connect(connectRef); } }, delayMillis); return; } } @Override public void disconnect() { resetAttempts(); this.state = State.DISCONNECTING; this.requestedState = State.OPENED; provider.disconnect(); } @Override public int getHRValue() { return provider.getHRValue(); } @Override public long getHRValueTimestamp() { return provider.getHRValueTimestamp(); } @Override public HRData getHRData() { return provider.getHRData(); } @Override public int getBatteryLevel() { return provider.getBatteryLevel(); } /*** HRClient interface */ @Override public void onDisconnectResult(boolean disconnectOK) { log("onDisonncetResult("+disconnectOK+")"); if (disconnectOK && state == State.CONNECTED && requestedState == State.CONNECTED) { /* this is unwanted disconnect, silently disconnect/connect */ state = State.DISCONNECTING; provider.disconnect(); return; } if (state == State.DISCONNECTING && requestedState == State.CONNECTED) { /* this is disconnected after unwanted disconnect, silently connect */ state = State.RECONNECTING; provider.connect(connectRef); return; } state = State.OPENED; requestedState = State.OPENED; if (client != null) client.onDisconnectResult(disconnectOK); } @Override public void onCloseResult(boolean closeOK) { state = State.CLOSED; requestedState = State.CLOSED; if (client != null) client.onConnectResult(closeOK); } @Override public void log(HRProvider src, String msg) { log(msg); } public void log(final String msg) { String res = "[ RetryingHRProviderProxy: " + provider.getProviderName() + ", attempt: " + Integer.toString(attempt) + " ]" + ", state: " + state + ", request: " + requestedState + ", " + msg; System.err.println(res); if (client != null) { if(Looper.myLooper() == Looper.getMainLooper()) { client.log(this, msg); } else { handler.post(new Runnable() { @Override public void run() { if (client != null) client.log(RetryingHRProviderProxy.this, msg); } }); } } } }