blob: 841a0418f9d0c6eaf93afec3706b6c7e8873f5ed [file] [log] [blame]
/*
* 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();
}
}
}