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