/* * 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.translate; 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.Message; import android.os.RemoteException; import android.util.Log; /** * Client-side interface to the central braille translator service. * * This class can be used to retrieve {@link BrailleTranslator} instances for * performing translation between text and braille cells. * * Typically, an instance of this class is created at application * initialization time and destroyed using the {@link destroy()} method when * the application is about to be destroyed. It is recommended that the * instance is destroyed and recreated if braille translation is not going to * be need for a long period of time. * * Threading:<br> * The object must be destroyed on the same thread it was created. * Other methods may be called from any thread. */ public class TranslatorManager { private static final String LOG_TAG = TranslatorManager.class.getSimpleName(); private static final String ACTION_TRANSLATOR_SERVICE = "com.googlecode.eyesfree.braille.service.ACTION_TRANSLATOR_SERVICE"; private static final Intent mServiceIntent = new Intent(ACTION_TRANSLATOR_SERVICE); /** * 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; public static final int ERROR = -1; public static final int SUCCESS = 0; /** * A callback interface to get notified when the translation * manager is ready to be used, or an error occurred during * initialization. */ public interface OnInitListener { /** * Called exactly once when it has been determined that the * translation service is either ready to be used ({@code SUCCESS}) * or the service is not available {@code ERROR}. */ public void onInit(int status); } private final Context mContext; private final TranslatorManagerHandler mHandler = new TranslatorManagerHandler(); private final ServiceCallback mServiceCallback = new ServiceCallback(); private OnInitListener mOnInitListener; private Connection mConnection; private int mNumFailedBinds = 0; /** * Constructs an instance. {@code context} is used to bind to the * translator service. The other methods of this class should not be * called (they will fail) until {@code onInitListener.onInit()} * is called. */ public TranslatorManager(Context context, OnInitListener onInitListener) { mContext = context; mOnInitListener = onInitListener; doBindService(); } /** * Destroys this instance, deallocating any global resources it is using. * Any {@link BrailleTranslator} objects that were created using this * object are invalid after this call. */ public void destroy() { doUnbindService(); mHandler.destroy(); } /** * Returns a new {@link BrailleTranslator} for the translation * table specified by {@code tableName}. */ // TODO: Document how to discover valid table names. public BrailleTranslator getTranslator(String tableName) { ITranslatorService localService = getTranslatorService(); if (localService != null) { try { if (localService.checkTable(tableName)) { return new BrailleTranslatorImpl(tableName); } } catch (RemoteException ex) { Log.e(LOG_TAG, "Error in getTranslator", ex); } } return null; } private void doBindService() { Connection localConnection = new Connection(); if (!mContext.bindService(mServiceIntent, localConnection, Context.BIND_AUTO_CREATE)) { Log.e(LOG_TAG, "Failed to bind to service"); mHandler.scheduleRebind(); return; } mConnection = localConnection; Log.i(LOG_TAG, "Bound to translator service"); } private void doUnbindService() { if (mConnection != null) { mContext.unbindService(mConnection); mConnection = null; } } private ITranslatorService getTranslatorService() { Connection localConnection = mConnection; if (localConnection != null) { return localConnection.mService; } return null; } private class Connection implements ServiceConnection { // Read in application threads, written in main thread. private volatile ITranslatorService mService; @Override public void onServiceConnected(ComponentName className, IBinder binder) { Log.i(LOG_TAG, "Connected to translation service"); ITranslatorService localService = ITranslatorService.Stub.asInterface(binder); try { localService.setCallback(mServiceCallback); mService = localService; synchronized (mHandler) { mNumFailedBinds = 0; } } catch (RemoteException ex) { // Service went away, rely on disconnect handler to // schedule a rebind. Log.e(LOG_TAG, "Error when setting callback", ex); } } @Override public void onServiceDisconnected(ComponentName className) { Log.e(LOG_TAG, "Disconnected from translator service"); mService = null; // Retry by rebinding, and finally call the onInit if aplicable. mHandler.scheduleRebind(); } } private class BrailleTranslatorImpl implements BrailleTranslator { private final String mTable; public BrailleTranslatorImpl(String table) { mTable = table; } @Override public byte[] translate(String text) { ITranslatorService localService = getTranslatorService(); if (localService != null) { try { return localService.translate(text, mTable); } catch (RemoteException ex) { Log.e(LOG_TAG, "Error in translate", ex); } } return null; } @Override public String backTranslate(byte[] cells) { ITranslatorService localService = getTranslatorService(); if (localService != null) { try { return localService.backTranslate(cells, mTable); } catch (RemoteException ex) { Log.e(LOG_TAG, "Error in backTranslate", ex); } } return null; } } private class ServiceCallback extends ITranslatorServiceCallback.Stub { @Override public void onInit(int status) { mHandler.onInit(status); } } private class TranslatorManagerHandler extends Handler { private static final int MSG_ON_INIT = 1; private static final int MSG_REBIND_SERVICE = 2; public void onInit(int status) { obtainMessage(MSG_ON_INIT, status, 0).sendToTarget(); } public void destroy() { mOnInitListener = null; // Cacnel outstanding messages, most importantly // scheduled rebinds. removeCallbacksAndMessages(null); } public void scheduleRebind() { synchronized (this) { if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) { int delay = REBIND_DELAY_MILLIS << mNumFailedBinds; sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay); ++mNumFailedBinds; } else { onInit(ERROR); } } } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ON_INIT: handleOnInit(msg.arg1); break; case MSG_REBIND_SERVICE: handleRebindService(); break; } } private void handleOnInit(int status) { if (mOnInitListener != null) { mOnInitListener.onInit(status); mOnInitListener = null; } } private void handleRebindService() { if (mConnection != null) { doUnbindService(); } doBindService(); } } }