Home | History | Annotate | Download | only in selfbraille
      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