1/*
2 * Copyright (C) 2012 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.googlecode.eyesfree.braille.display;
18
19import android.os.Message;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.ServiceConnection;
24import android.os.Handler;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.util.Log;
28
29/**
30 * A client for the braille display service.
31 */
32public class Display {
33    private static final String LOG_TAG = Display.class.getSimpleName();
34    /** Service name used for connecting to the service. */
35    public static final String ACTION_DISPLAY_SERVICE =
36            "com.googlecode.eyesfree.braille.service.ACTION_DISPLAY_SERVICE";
37
38    /** Initial value, which is never reported to the listener. */
39    private static final int STATE_UNKNOWN = -2;
40    public static final int STATE_ERROR = -1;
41    public static final int STATE_NOT_CONNECTED = 0;
42    public static final int STATE_CONNECTED = 1;
43
44    private final OnConnectionStateChangeListener
45            mConnectionStateChangeListener;
46    private final Context mContext;
47    private final DisplayHandler mHandler;
48    private volatile OnInputEventListener mInputEventListener;
49    private static final Intent mServiceIntent =
50            new Intent(ACTION_DISPLAY_SERVICE);
51    private Connection mConnection;
52    private int currentConnectionState = STATE_UNKNOWN;
53    private BrailleDisplayProperties mDisplayProperties;
54    private ServiceCallback mServiceCallback = new ServiceCallback();
55    /**
56     * Delay before the first rebind attempt on bind error or service
57     * disconnect.
58     */
59    private static final int REBIND_DELAY_MILLIS = 500;
60    private static final int MAX_REBIND_ATTEMPTS = 5;
61    private int mNumFailedBinds = 0;
62
63    /**
64     * A callback interface to get informed about connection state changes.
65     */
66    public interface OnConnectionStateChangeListener {
67        void onConnectionStateChanged(int state);
68    }
69
70    /**
71     * A callback interface for input from the braille display.
72     */
73    public interface OnInputEventListener {
74        void onInputEvent(BrailleInputEvent inputEvent);
75    }
76
77    /**
78     * Constructs an instance and connects to the braille display service.
79     * The current thread must have an {@link android.os.Looper} associated
80     * with it.  Callbacks from this object will all be executed on the
81     * current thread.  Connection state will be reported to {@code listener).
82     */
83    public Display(Context context, OnConnectionStateChangeListener listener) {
84        this(context, listener, null);
85    }
86
87    /**
88     * Constructs an instance and connects to the braille display service.
89     * Callbacks from this object will all be executed on the thread
90     * associated with {@code handler}.  If {@code handler} is {@code null},
91     * the current thread must have an {@link android.os.Looper} associated
92     * with it, which will then be used to execute callbacks.  Connection
93     * state will be reported to {@code listener).
94     */
95    public Display(Context context, OnConnectionStateChangeListener listener,
96            Handler handler) {
97        mContext = context;
98        mConnectionStateChangeListener = listener;
99        if (handler == null) {
100            mHandler = new DisplayHandler();
101        } else {
102            mHandler = new DisplayHandler(handler);
103        }
104
105        doBindService();
106    }
107
108    /**
109     * Sets a {@code listener} for input events.  {@code listener} can be
110     * {@code null} to remove a previously set listener.
111     */
112    public void setOnInputEventListener(OnInputEventListener listener) {
113        mInputEventListener = listener;
114    }
115
116    /**
117     * Returns the display properties, or {@code null} if not connected
118     * to a display.
119     */
120    public BrailleDisplayProperties getDisplayProperties() {
121        return mDisplayProperties;
122    }
123
124    /**
125     * Displays a given dots configuration on the braille display.
126     * @param patterns Dots configuration to be displayed.
127     */
128    public void displayDots(byte[] patterns) {
129        IBrailleService localService = getBrailleService();
130        if (localService != null) {
131            try {
132                localService.displayDots(patterns);
133            } catch (RemoteException ex) {
134                Log.e(LOG_TAG, "Error in displayDots", ex);
135            }
136        } else {
137            Log.v(LOG_TAG, "Error in displayDots: service not connected");
138        }
139    }
140
141    /**
142     * Unbinds from the braille display service and deallocates any
143     * resources.  This method should be called when the braille display
144     * is no longer in use by this client.
145     */
146    public void shutdown() {
147        doUnbindService();
148    }
149
150    // NOTE: The methods in this class will be executed in the main
151    // application thread.
152    private class Connection implements ServiceConnection {
153        private volatile IBrailleService mService;
154
155        @Override
156        public void onServiceConnected(ComponentName className,
157                IBinder binder) {
158            Log.i(LOG_TAG, "Connected to braille service");
159            IBrailleService localService =
160                IBrailleService.Stub.asInterface(binder);
161            try {
162                localService.registerCallback(mServiceCallback);
163                mService = localService;
164                synchronized (mHandler) {
165                    mNumFailedBinds = 0;
166                }
167            } catch (RemoteException e) {
168                // In this case the service has crashed before we could even do
169                // anything with it.
170                Log.e(LOG_TAG, "Failed to register callback on service", e);
171                // We should get a disconnected call and the rebind
172                // and failure reporting happens in that handler.
173            }
174        }
175
176        @Override
177        public void onServiceDisconnected(ComponentName className) {
178            mService = null;
179            Log.e(LOG_TAG, "Disconnected from braille service");
180            // Report display disconnected for now, this will turn into a
181            // connected state or error state depending on how the retrying
182            // goes.
183            mHandler.reportConnectionState(STATE_NOT_CONNECTED, null);
184            mHandler.scheduleRebind();
185        }
186    }
187
188    // NOTE: The methods of this class will be executed in the IPC
189    // thread pool and not on the main application thread.
190    private class ServiceCallback extends IBrailleServiceCallback.Stub {
191        @Override
192        public void onDisplayConnected(
193            BrailleDisplayProperties displayProperties) {
194            mHandler.reportConnectionState(STATE_CONNECTED, displayProperties);
195        }
196
197        @Override
198        public void onDisplayDisconnected() {
199            mHandler.reportConnectionState(STATE_NOT_CONNECTED, null);
200        }
201
202        @Override
203        public void onInput(BrailleInputEvent inputEvent) {
204            mHandler.reportInputEvent(inputEvent);
205        }
206    }
207
208    private void doBindService() {
209        Connection localConnection = new Connection();
210        if (!mContext.bindService(mServiceIntent, localConnection,
211                Context.BIND_AUTO_CREATE)) {
212            Log.e(LOG_TAG, "Failed to bind Service");
213            mHandler.scheduleRebind();
214            return;
215        }
216        mConnection = localConnection;
217        Log.i(LOG_TAG, "Bound to braille service");
218    }
219
220    private void doUnbindService() {
221        IBrailleService localService = getBrailleService();
222        if (localService != null) {
223            try {
224                localService.unregisterCallback(mServiceCallback);
225            } catch (RemoteException e) {
226                // Nothing to do if the service can't be reached.
227            }
228        }
229        if (mConnection != null) {
230            mContext.unbindService(mConnection);
231            mConnection = null;
232        }
233    }
234
235    private IBrailleService getBrailleService() {
236        Connection localConnection = mConnection;
237        if (localConnection != null) {
238            return localConnection.mService;
239        }
240        return null;
241    }
242
243    private class DisplayHandler extends Handler {
244        private static final int MSG_REPORT_CONNECTION_STATE = 1;
245        private static final int MSG_REPORT_INPUT_EVENT = 2;
246        private static final int MSG_REBIND_SERVICE = 3;
247
248        public DisplayHandler() {
249        }
250
251        public DisplayHandler(Handler handler) {
252            super(handler.getLooper());
253        }
254
255        public void reportConnectionState(final int newState,
256                final BrailleDisplayProperties displayProperties) {
257            obtainMessage(MSG_REPORT_CONNECTION_STATE, newState, 0,
258                    displayProperties)
259                    .sendToTarget();
260        }
261
262        public void reportInputEvent(BrailleInputEvent event) {
263            obtainMessage(MSG_REPORT_INPUT_EVENT, event).sendToTarget();
264        }
265
266        public void scheduleRebind() {
267            synchronized (this) {
268                if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
269                    int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
270                    sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
271                    ++mNumFailedBinds;
272                    Log.w(LOG_TAG, String.format(
273                        "Will rebind to braille service in %d ms.", delay));
274                } else {
275                    reportConnectionState(STATE_ERROR, null);
276                }
277            }
278        }
279
280        @Override
281        public void handleMessage(Message msg) {
282            switch (msg.what) {
283                case MSG_REPORT_CONNECTION_STATE:
284                    handleReportConnectionState(msg.arg1,
285                            (BrailleDisplayProperties) msg.obj);
286                    break;
287                case MSG_REPORT_INPUT_EVENT:
288                    handleReportInputEvent((BrailleInputEvent) msg.obj);
289                    break;
290                case MSG_REBIND_SERVICE:
291                    handleRebindService();
292                    break;
293            }
294        }
295
296        private void handleReportConnectionState(int newState,
297                BrailleDisplayProperties displayProperties) {
298            mDisplayProperties = displayProperties;
299            if (newState != currentConnectionState
300                    && mConnectionStateChangeListener != null) {
301                mConnectionStateChangeListener.onConnectionStateChanged(
302                    newState);
303            }
304            currentConnectionState = newState;
305        }
306
307        private void handleReportInputEvent(BrailleInputEvent event) {
308            OnInputEventListener localListener = mInputEventListener;
309            if (localListener != null) {
310                localListener.onInputEvent(event);
311            }
312        }
313
314        private void handleRebindService() {
315            if (mConnection != null) {
316                doUnbindService();
317            }
318            doBindService();
319        }
320    }
321}
322