/*
* 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.selfbraille;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Client-side interface to the self brailling interface.
*
* Threading: Instances of this object should be created and shut down
* in a thread with a {@link Looper} associated with it. Other methods may
* be called on any thread.
*/
public class SelfBrailleClient {
private static final String LOG_TAG =
SelfBrailleClient.class.getSimpleName();
private static final String ACTION_SELF_BRAILLE_SERVICE =
"com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE";
private static final String BRAILLE_BACK_PACKAGE =
"com.googlecode.eyesfree.brailleback";
private static final Intent mServiceIntent =
new Intent(ACTION_SELF_BRAILLE_SERVICE)
.setPackage(BRAILLE_BACK_PACKAGE);
/**
* SHA-1 hash value of the Eyes-Free release key certificate, used to sign
* BrailleBack. It was generated from the keystore with:
* $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \
* > cert
* $ keytool -printcert -file cert
*/
// The typecasts are to silence a compiler warning about loss of precision
private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] {
(byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D,
(byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4,
(byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B,
(byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76,
(byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61
};
/**
* 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;
private final Binder mIdentity = new Binder();
private final Context mContext;
private final boolean mAllowDebugService;
private final SelfBrailleHandler mHandler = new SelfBrailleHandler();
private boolean mShutdown = false;
/**
* Written in handler thread, read in any thread calling methods on the
* object.
*/
private volatile Connection mConnection;
/** Protected by synchronizing on mHandler. */
private int mNumFailedBinds = 0;
/**
* Constructs an instance of this class. {@code context} is used to bind
* to the self braille service. The current thread must have a Looper
* associated with it. If {@code allowDebugService} is true, this instance
* will connect to a BrailleBack service without requiring it to be signed
* by the release key used to sign BrailleBack.
*/
public SelfBrailleClient(Context context, boolean allowDebugService) {
mContext = context;
mAllowDebugService = allowDebugService;
doBindService();
}
/**
* Shuts this instance down, deallocating any global resources it is using.
* This method must be called on the same thread that created this object.
*/
public void shutdown() {
mShutdown = true;
doUnbindService();
}
public void write(WriteData writeData) {
writeData.validate();
ISelfBrailleService localService = getSelfBrailleService();
if (localService != null) {
try {
localService.write(mIdentity, writeData);
} catch (RemoteException ex) {
Log.e(LOG_TAG, "Self braille write failed", ex);
}
}
}
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 self braille service");
}
private void doUnbindService() {
if (mConnection != null) {
ISelfBrailleService localService = getSelfBrailleService();
if (localService != null) {
try {
localService.disconnect(mIdentity);
} catch (RemoteException ex) {
// Nothing to do.
}
}
mContext.unbindService(mConnection);
mConnection = null;
}
}
private ISelfBrailleService getSelfBrailleService() {
Connection localConnection = mConnection;
if (localConnection != null) {
return localConnection.mService;
}
return null;
}
private boolean verifyPackage() {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi;
try {
pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE,
PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException ex) {
Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE,
ex);
return false;
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException ex) {
Log.e(LOG_TAG, "SHA-1 not supported", ex);
return false;
}
// Check if any of the certificates match our hash.
for (Signature signature : pi.signatures) {
digest.update(signature.toByteArray());
if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) {
return true;
}
digest.reset();
}
if (mAllowDebugService) {
Log.w(LOG_TAG, String.format(
"*** %s connected to BrailleBack with invalid (debug?) "
+ "signature ***",
mContext.getPackageName()));
return true;
}
return false;
}
private class Connection implements ServiceConnection {
// Read in application threads, written in main thread.
private volatile ISelfBrailleService mService;
@Override
public void onServiceConnected(ComponentName className,
IBinder binder) {
if (!verifyPackage()) {
Log.w(LOG_TAG, String.format("Service certificate mismatch "
+ "for %s, dropping connection",
BRAILLE_BACK_PACKAGE));
mHandler.unbindService();
return;
}
Log.i(LOG_TAG, "Connected to self braille service");
mService = ISelfBrailleService.Stub.asInterface(binder);
synchronized (mHandler) {
mNumFailedBinds = 0;
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.e(LOG_TAG, "Disconnected from self braille service");
mService = null;
// Retry by rebinding.
mHandler.scheduleRebind();
}
}
private class SelfBrailleHandler extends Handler {
private static final int MSG_REBIND_SERVICE = 1;
private static final int MSG_UNBIND_SERVICE = 2;
public void scheduleRebind() {
synchronized (this) {
if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
++mNumFailedBinds;
}
}
}
public void unbindService() {
sendEmptyMessage(MSG_UNBIND_SERVICE);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REBIND_SERVICE:
handleRebindService();
break;
case MSG_UNBIND_SERVICE:
handleUnbindService();
break;
}
}
private void handleRebindService() {
if (mShutdown) {
return;
}
if (mConnection != null) {
doUnbindService();
}
doBindService();
}
private void handleUnbindService() {
doUnbindService();
}
}
}