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
17package com.googlecode.eyesfree.braille.selfbraille;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.Signature;
26import android.os.Binder;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Message;
30import android.os.RemoteException;
31import android.util.Log;
32
33import java.security.MessageDigest;
34import 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 */
43public 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