/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.eyesfree.braille.display;
import android.os.Message;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
/**
* A client for the braille display service.
*/
public class Display {
private static final String LOG_TAG = Display.class.getSimpleName();
/** Service name used for connecting to the service. */
public static final String ACTION_DISPLAY_SERVICE =
"com.googlecode.eyesfree.braille.service.ACTION_DISPLAY_SERVICE";
/** Initial value, which is never reported to the listener. */
private static final int STATE_UNKNOWN = -2;
public static final int STATE_ERROR = -1;
public static final int STATE_NOT_CONNECTED = 0;
public static final int STATE_CONNECTED = 1;
private final OnConnectionStateChangeListener
mConnectionStateChangeListener;
private final Context mContext;
private final DisplayHandler mHandler;
private volatile OnInputEventListener mInputEventListener;
private static final Intent mServiceIntent =
new Intent(ACTION_DISPLAY_SERVICE);
private Connection mConnection;
private int currentConnectionState = STATE_UNKNOWN;
private BrailleDisplayProperties mDisplayProperties;
private ServiceCallback mServiceCallback = new ServiceCallback();
/**
* Delay before the first rebind attempt on bind error or service
* disconnect.
*/
private static final int REBIND_DELAY_MILLIS = 500;
private static final int MAX_REBIND_ATTEMPTS = 5;
private int mNumFailedBinds = 0;
/**
* A callback interface to get informed about connection state changes.
*/
public interface OnConnectionStateChangeListener {
void onConnectionStateChanged(int state);
}
/**
* A callback interface for input from the braille display.
*/
public interface OnInputEventListener {
void onInputEvent(BrailleInputEvent inputEvent);
}
/**
* Constructs an instance and connects to the braille display service.
* The current thread must have an {@link android.os.Looper} associated
* with it. Callbacks from this object will all be executed on the
* current thread. Connection state will be reported to {@code listener).
*/
public Display(Context context, OnConnectionStateChangeListener listener) {
this(context, listener, null);
}
/**
* Constructs an instance and connects to the braille display service.
* Callbacks from this object will all be executed on the thread
* associated with {@code handler}. If {@code handler} is {@code null},
* the current thread must have an {@link android.os.Looper} associated
* with it, which will then be used to execute callbacks. Connection
* state will be reported to {@code listener).
*/
public Display(Context context, OnConnectionStateChangeListener listener,
Handler handler) {
mContext = context;
mConnectionStateChangeListener = listener;
if (handler == null) {
mHandler = new DisplayHandler();
} else {
mHandler = new DisplayHandler(handler);
}
doBindService();
}
/**
* Sets a {@code listener} for input events. {@code listener} can be
* {@code null} to remove a previously set listener.
*/
public void setOnInputEventListener(OnInputEventListener listener) {
mInputEventListener = listener;
}
/**
* Returns the display properties, or {@code null} if not connected
* to a display.
*/
public BrailleDisplayProperties getDisplayProperties() {
return mDisplayProperties;
}
/**
* Displays a given dots configuration on the braille display.
* @param patterns Dots configuration to be displayed.
*/
public void displayDots(byte[] patterns) {
IBrailleService localService = getBrailleService();
if (localService != null) {
try {
localService.displayDots(patterns);
} catch (RemoteException ex) {
Log.e(LOG_TAG, "Error in displayDots", ex);
}
} else {
Log.v(LOG_TAG, "Error in displayDots: service not connected");
}
}
/**
* Unbinds from the braille display service and deallocates any
* resources. This method should be called when the braille display
* is no longer in use by this client.
*/
public void shutdown() {
doUnbindService();
}
// NOTE: The methods in this class will be executed in the main
// application thread.
private class Connection implements ServiceConnection {
private volatile IBrailleService mService;
@Override
public void onServiceConnected(ComponentName className,
IBinder binder) {
Log.i(LOG_TAG, "Connected to braille service");
IBrailleService localService =
IBrailleService.Stub.asInterface(binder);
try {
localService.registerCallback(mServiceCallback);
mService = localService;
synchronized (mHandler) {
mNumFailedBinds = 0;
}
} catch (RemoteException e) {
// In this case the service has crashed before we could even do
// anything with it.
Log.e(LOG_TAG, "Failed to register callback on service", e);
// We should get a disconnected call and the rebind
// and failure reporting happens in that handler.
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
mService = null;
Log.e(LOG_TAG, "Disconnected from braille service");
// Report display disconnected for now, this will turn into a
// connected state or error state depending on how the retrying
// goes.
mHandler.reportConnectionState(STATE_NOT_CONNECTED, null);
mHandler.scheduleRebind();
}
}
// NOTE: The methods of this class will be executed in the IPC
// thread pool and not on the main application thread.
private class ServiceCallback extends IBrailleServiceCallback.Stub {
@Override
public void onDisplayConnected(
BrailleDisplayProperties displayProperties) {
mHandler.reportConnectionState(STATE_CONNECTED, displayProperties);
}
@Override
public void onDisplayDisconnected() {
mHandler.reportConnectionState(STATE_NOT_CONNECTED, null);
}
@Override
public void onInput(BrailleInputEvent inputEvent) {
mHandler.reportInputEvent(inputEvent);
}
}
private void doBindService() {
Connection localConnection = new Connection();
if (!mContext.bindService(mServiceIntent, localConnection,
Context.BIND_AUTO_CREATE)) {
Log.e(LOG_TAG, "Failed to bind Service");
mHandler.scheduleRebind();
return;
}
mConnection = localConnection;
Log.i(LOG_TAG, "Bound to braille service");
}
private void doUnbindService() {
IBrailleService localService = getBrailleService();
if (localService != null) {
try {
localService.unregisterCallback(mServiceCallback);
} catch (RemoteException e) {
// Nothing to do if the service can't be reached.
}
}
if (mConnection != null) {
mContext.unbindService(mConnection);
mConnection = null;
}
}
private IBrailleService getBrailleService() {
Connection localConnection = mConnection;
if (localConnection != null) {
return localConnection.mService;
}
return null;
}
private class DisplayHandler extends Handler {
private static final int MSG_REPORT_CONNECTION_STATE = 1;
private static final int MSG_REPORT_INPUT_EVENT = 2;
private static final int MSG_REBIND_SERVICE = 3;
public DisplayHandler() {
}
public DisplayHandler(Handler handler) {
super(handler.getLooper());
}
public void reportConnectionState(final int newState,
final BrailleDisplayProperties displayProperties) {
obtainMessage(MSG_REPORT_CONNECTION_STATE, newState, 0,
displayProperties)
.sendToTarget();
}
public void reportInputEvent(BrailleInputEvent event) {
obtainMessage(MSG_REPORT_INPUT_EVENT, event).sendToTarget();
}
public void scheduleRebind() {
synchronized (this) {
if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
++mNumFailedBinds;
Log.w(LOG_TAG, String.format(
"Will rebind to braille service in %d ms.", delay));
} else {
reportConnectionState(STATE_ERROR, null);
}
}
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REPORT_CONNECTION_STATE:
handleReportConnectionState(msg.arg1,
(BrailleDisplayProperties) msg.obj);
break;
case MSG_REPORT_INPUT_EVENT:
handleReportInputEvent((BrailleInputEvent) msg.obj);
break;
case MSG_REBIND_SERVICE:
handleRebindService();
break;
}
}
private void handleReportConnectionState(int newState,
BrailleDisplayProperties displayProperties) {
mDisplayProperties = displayProperties;
if (newState != currentConnectionState
&& mConnectionStateChangeListener != null) {
mConnectionStateChangeListener.onConnectionStateChanged(
newState);
}
currentConnectionState = newState;
}
private void handleReportInputEvent(BrailleInputEvent event) {
OnInputEventListener localListener = mInputEventListener;
if (localListener != null) {
localListener.onInputEvent(event);
}
}
private void handleRebindService() {
if (mConnection != null) {
doUnbindService();
}
doBindService();
}
}
}