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.translate;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.os.Handler;
24import android.os.IBinder;
25import android.os.Message;
26import android.os.RemoteException;
27import android.util.Log;
28
29/**
30 * Client-side interface to the central braille translator service.
31 *
32 * This class can be used to retrieve {@link BrailleTranslator} instances for
33 * performing translation between text and braille cells.
34 *
35 * Typically, an instance of this class is created at application
36 * initialization time and destroyed using the {@link destroy()} method when
37 * the application is about to be destroyed.  It is recommended that the
38 * instance is destroyed and recreated if braille translation is not going to
39 * be need for a long period of time.
40 *
41 * Threading:<br>
42 * The object must be destroyed on the same thread it was created.
43 * Other methods may be called from any thread.
44 */
45public class TranslatorManager {
46    private static final String LOG_TAG =
47            TranslatorManager.class.getSimpleName();
48    private static final String ACTION_TRANSLATOR_SERVICE =
49            "com.googlecode.eyesfree.braille.service.ACTION_TRANSLATOR_SERVICE";
50    private static final Intent mServiceIntent =
51            new Intent(ACTION_TRANSLATOR_SERVICE);
52    /**
53     * Delay before the first rebind attempt on bind error or service
54     * disconnect.
55     */
56    private static final int REBIND_DELAY_MILLIS = 500;
57    private static final int MAX_REBIND_ATTEMPTS = 5;
58    public static final int ERROR = -1;
59    public static final int SUCCESS = 0;
60
61    /**
62     * A callback interface to get notified when the translation
63     * manager is ready to be used, or an error occurred during
64     * initialization.
65     */
66    public interface OnInitListener {
67        /**
68         * Called exactly once when it has been determined that the
69         * translation service is either ready to be used ({@code SUCCESS})
70         * or the service is not available {@code ERROR}.
71         */
72        public void onInit(int status);
73    }
74
75    private final Context mContext;
76    private final TranslatorManagerHandler mHandler =
77            new TranslatorManagerHandler();
78    private final ServiceCallback mServiceCallback = new ServiceCallback();
79
80    private OnInitListener mOnInitListener;
81    private Connection mConnection;
82    private int mNumFailedBinds = 0;
83
84    /**
85     * Constructs an instance.  {@code context} is used to bind to the
86     * translator service.  The other methods of this class should not be
87     * called (they will fail) until {@code onInitListener.onInit()}
88     * is called.
89     */
90    public TranslatorManager(Context context, OnInitListener onInitListener) {
91        mContext = context;
92        mOnInitListener = onInitListener;
93        doBindService();
94    }
95
96    /**
97     * Destroys this instance, deallocating any global resources it is using.
98     * Any {@link BrailleTranslator} objects that were created using this
99     * object are invalid after this call.
100     */
101    public void destroy() {
102        doUnbindService();
103        mHandler.destroy();
104    }
105
106    /**
107     * Returns a new {@link BrailleTranslator} for the translation
108     * table specified by {@code tableName}.
109     */
110    // TODO: Document how to discover valid table names.
111    public BrailleTranslator getTranslator(String tableName) {
112        ITranslatorService localService = getTranslatorService();
113        if (localService != null) {
114            try {
115                if (localService.checkTable(tableName)) {
116                    return new BrailleTranslatorImpl(tableName);
117                }
118            } catch (RemoteException ex) {
119                Log.e(LOG_TAG, "Error in getTranslator", ex);
120            }
121        }
122        return null;
123    }
124
125    private void doBindService() {
126        Connection localConnection = new Connection();
127        if (!mContext.bindService(mServiceIntent, localConnection,
128                Context.BIND_AUTO_CREATE)) {
129            Log.e(LOG_TAG, "Failed to bind to service");
130            mHandler.scheduleRebind();
131            return;
132        }
133        mConnection = localConnection;
134        Log.i(LOG_TAG, "Bound to translator service");
135    }
136
137    private void doUnbindService() {
138        if (mConnection != null) {
139            mContext.unbindService(mConnection);
140            mConnection = null;
141        }
142    }
143
144    private ITranslatorService getTranslatorService() {
145        Connection localConnection = mConnection;
146        if (localConnection != null) {
147            return localConnection.mService;
148        }
149        return null;
150    }
151
152    private class Connection implements ServiceConnection {
153        // Read in application threads, written in main thread.
154        private volatile ITranslatorService mService;
155
156        @Override
157        public void onServiceConnected(ComponentName className,
158                IBinder binder) {
159            Log.i(LOG_TAG, "Connected to translation service");
160            ITranslatorService localService =
161                    ITranslatorService.Stub.asInterface(binder);
162            try {
163                localService.setCallback(mServiceCallback);
164                mService = localService;
165                synchronized (mHandler) {
166                    mNumFailedBinds = 0;
167                }
168            } catch (RemoteException ex) {
169                // Service went away, rely on disconnect handler to
170                // schedule a rebind.
171                Log.e(LOG_TAG, "Error when setting callback", ex);
172            }
173        }
174
175        @Override
176        public void onServiceDisconnected(ComponentName className) {
177            Log.e(LOG_TAG, "Disconnected from translator service");
178            mService = null;
179            // Retry by rebinding, and finally call the onInit if aplicable.
180            mHandler.scheduleRebind();
181        }
182    }
183
184    private class BrailleTranslatorImpl implements BrailleTranslator {
185        private final String mTable;
186
187        public BrailleTranslatorImpl(String table) {
188            mTable = table;
189        }
190
191        @Override
192        public byte[] translate(String text) {
193            ITranslatorService localService = getTranslatorService();
194            if (localService != null) {
195                try {
196                    return localService.translate(text, mTable);
197                } catch (RemoteException ex) {
198                    Log.e(LOG_TAG, "Error in translate", ex);
199                }
200            }
201            return null;
202        }
203
204        @Override
205        public String backTranslate(byte[] cells) {
206            ITranslatorService localService = getTranslatorService();
207            if (localService != null) {
208                try {
209                    return localService.backTranslate(cells, mTable);
210                } catch (RemoteException ex) {
211                    Log.e(LOG_TAG, "Error in backTranslate", ex);
212                }
213            }
214            return null;
215        }
216    }
217
218    private class ServiceCallback extends ITranslatorServiceCallback.Stub {
219        @Override
220        public void onInit(int status) {
221            mHandler.onInit(status);
222        }
223    }
224
225    private class TranslatorManagerHandler extends Handler {
226        private static final int MSG_ON_INIT = 1;
227        private static final int MSG_REBIND_SERVICE = 2;
228
229        public void onInit(int status) {
230            obtainMessage(MSG_ON_INIT, status, 0).sendToTarget();
231        }
232
233        public void destroy() {
234            mOnInitListener = null;
235            // Cacnel outstanding messages, most importantly
236            // scheduled rebinds.
237            removeCallbacksAndMessages(null);
238        }
239
240        public void scheduleRebind() {
241            synchronized (this) {
242                if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
243                    int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
244                    sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
245                    ++mNumFailedBinds;
246                } else {
247                    onInit(ERROR);
248                }
249            }
250        }
251
252        @Override
253        public void handleMessage(Message msg) {
254            switch (msg.what) {
255                case MSG_ON_INIT:
256                    handleOnInit(msg.arg1);
257                    break;
258                case MSG_REBIND_SERVICE:
259                    handleRebindService();
260                    break;
261            }
262        }
263
264        private void handleOnInit(int status) {
265            if (mOnInitListener != null) {
266                mOnInitListener.onInit(status);
267                mOnInitListener = null;
268            }
269        }
270
271        private void handleRebindService() {
272            if (mConnection != null) {
273                doUnbindService();
274            }
275            doBindService();
276        }
277    }
278}
279