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