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 17 package com.googlecode.eyesfree.braille.selfbraille; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.ServiceConnection; 23 import android.content.pm.PackageInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.Signature; 26 import android.os.Binder; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.Message; 30 import android.os.RemoteException; 31 import android.util.Log; 32 33 import java.security.MessageDigest; 34 import java.security.NoSuchAlgorithmException; 35 36 /** 37 * Client-side interface to the self brailling interface. 38 * 39 * Threading: Instances of this object should be created and shut down 40 * in a thread with a {@link Looper} associated with it. Other methods may 41 * be called on any thread. 42 */ 43 public class SelfBrailleClient { 44 private static final String LOG_TAG = 45 SelfBrailleClient.class.getSimpleName(); 46 private static final String ACTION_SELF_BRAILLE_SERVICE = 47 "com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE"; 48 private static final String BRAILLE_BACK_PACKAGE = 49 "com.googlecode.eyesfree.brailleback"; 50 private static final Intent mServiceIntent = 51 new Intent(ACTION_SELF_BRAILLE_SERVICE) 52 .setPackage(BRAILLE_BACK_PACKAGE); 53 /** 54 * SHA-1 hash value of the Eyes-Free release key certificate, used to sign 55 * BrailleBack. It was generated from the keystore with: 56 * $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \ 57 * > cert 58 * $ keytool -printcert -file cert 59 */ 60 // The typecasts are to silence a compiler warning about loss of precision 61 private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] { 62 (byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D, 63 (byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4, 64 (byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B, 65 (byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76, 66 (byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61 67 }; 68 /** 69 * Delay before the first rebind attempt on bind error or service 70 * disconnect. 71 */ 72 private static final int REBIND_DELAY_MILLIS = 500; 73 private static final int MAX_REBIND_ATTEMPTS = 5; 74 75 private final Binder mIdentity = new Binder(); 76 private final Context mContext; 77 private final boolean mAllowDebugService; 78 private final SelfBrailleHandler mHandler = new SelfBrailleHandler(); 79 private boolean mShutdown = false; 80 81 /** 82 * Written in handler thread, read in any thread calling methods on the 83 * object. 84 */ 85 private volatile Connection mConnection; 86 /** Protected by synchronizing on mHandler. */ 87 private int mNumFailedBinds = 0; 88 89 /** 90 * Constructs an instance of this class. {@code context} is used to bind 91 * to the self braille service. The current thread must have a Looper 92 * associated with it. If {@code allowDebugService} is true, this instance 93 * will connect to a BrailleBack service without requiring it to be signed 94 * by the release key used to sign BrailleBack. 95 */ 96 public SelfBrailleClient(Context context, boolean allowDebugService) { 97 mContext = context; 98 mAllowDebugService = allowDebugService; 99 doBindService(); 100 } 101 102 /** 103 * Shuts this instance down, deallocating any global resources it is using. 104 * This method must be called on the same thread that created this object. 105 */ 106 public void shutdown() { 107 mShutdown = true; 108 doUnbindService(); 109 } 110 111 public void write(WriteData writeData) { 112 writeData.validate(); 113 ISelfBrailleService localService = getSelfBrailleService(); 114 if (localService != null) { 115 try { 116 localService.write(mIdentity, writeData); 117 } catch (RemoteException ex) { 118 Log.e(LOG_TAG, "Self braille write failed", ex); 119 } 120 } 121 } 122 123 private void doBindService() { 124 Connection localConnection = new Connection(); 125 if (!mContext.bindService(mServiceIntent, localConnection, 126 Context.BIND_AUTO_CREATE)) { 127 Log.e(LOG_TAG, "Failed to bind to service"); 128 mHandler.scheduleRebind(); 129 return; 130 } 131 mConnection = localConnection; 132 Log.i(LOG_TAG, "Bound to self braille service"); 133 } 134 135 private void doUnbindService() { 136 if (mConnection != null) { 137 ISelfBrailleService localService = getSelfBrailleService(); 138 if (localService != null) { 139 try { 140 localService.disconnect(mIdentity); 141 } catch (RemoteException ex) { 142 // Nothing to do. 143 } 144 } 145 mContext.unbindService(mConnection); 146 mConnection = null; 147 } 148 } 149 150 private ISelfBrailleService getSelfBrailleService() { 151 Connection localConnection = mConnection; 152 if (localConnection != null) { 153 return localConnection.mService; 154 } 155 return null; 156 } 157 158 private boolean verifyPackage() { 159 PackageManager pm = mContext.getPackageManager(); 160 PackageInfo pi; 161 try { 162 pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE, 163 PackageManager.GET_SIGNATURES); 164 } catch (PackageManager.NameNotFoundException ex) { 165 Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE, 166 ex); 167 return false; 168 } 169 MessageDigest digest; 170 try { 171 digest = MessageDigest.getInstance("SHA-1"); 172 } catch (NoSuchAlgorithmException ex) { 173 Log.e(LOG_TAG, "SHA-1 not supported", ex); 174 return false; 175 } 176 // Check if any of the certificates match our hash. 177 for (Signature signature : pi.signatures) { 178 digest.update(signature.toByteArray()); 179 if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) { 180 return true; 181 } 182 digest.reset(); 183 } 184 if (mAllowDebugService) { 185 Log.w(LOG_TAG, String.format( 186 "*** %s connected to BrailleBack with invalid (debug?) " 187 + "signature ***", 188 mContext.getPackageName())); 189 return true; 190 } 191 return false; 192 } 193 private class Connection implements ServiceConnection { 194 // Read in application threads, written in main thread. 195 private volatile ISelfBrailleService mService; 196 197 @Override 198 public void onServiceConnected(ComponentName className, 199 IBinder binder) { 200 if (!verifyPackage()) { 201 Log.w(LOG_TAG, String.format("Service certificate mismatch " 202 + "for %s, dropping connection", 203 BRAILLE_BACK_PACKAGE)); 204 mHandler.unbindService(); 205 return; 206 } 207 Log.i(LOG_TAG, "Connected to self braille service"); 208 mService = ISelfBrailleService.Stub.asInterface(binder); 209 synchronized (mHandler) { 210 mNumFailedBinds = 0; 211 } 212 } 213 214 @Override 215 public void onServiceDisconnected(ComponentName className) { 216 Log.e(LOG_TAG, "Disconnected from self braille service"); 217 mService = null; 218 // Retry by rebinding. 219 mHandler.scheduleRebind(); 220 } 221 } 222 223 private class SelfBrailleHandler extends Handler { 224 private static final int MSG_REBIND_SERVICE = 1; 225 private static final int MSG_UNBIND_SERVICE = 2; 226 227 public void scheduleRebind() { 228 synchronized (this) { 229 if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) { 230 int delay = REBIND_DELAY_MILLIS << mNumFailedBinds; 231 sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay); 232 ++mNumFailedBinds; 233 } 234 } 235 } 236 237 public void unbindService() { 238 sendEmptyMessage(MSG_UNBIND_SERVICE); 239 } 240 241 @Override 242 public void handleMessage(Message msg) { 243 switch (msg.what) { 244 case MSG_REBIND_SERVICE: 245 handleRebindService(); 246 break; 247 case MSG_UNBIND_SERVICE: 248 handleUnbindService(); 249 break; 250 } 251 } 252 253 private void handleRebindService() { 254 if (mShutdown) { 255 return; 256 } 257 if (mConnection != null) { 258 doUnbindService(); 259 } 260 doBindService(); 261 } 262 263 private void handleUnbindService() { 264 doUnbindService(); 265 } 266 } 267 } 268