bradfitz / android-garage-opener
- Source
- Commits
- Network (8)
- Issues (0)
- Downloads (0)
- Wiki (1)
- Graphs
-
Tree:
eda5625
Brad Fitzpatrick (author)
Sun Nov 16 11:34:32 -0800 2008
| eda56259 » | Brad Fitzpatrick | 2008-11-16 | 1 | package com.danga.garagedoor; | |
| 2 | |||||
| 3 | import android.app.Notification; | ||||
| 4 | import android.app.NotificationManager; | ||||
| 5 | import android.app.PendingIntent; | ||||
| 6 | import android.app.Service; | ||||
| 7 | import android.content.BroadcastReceiver; | ||||
| 8 | import android.content.Context; | ||||
| 9 | import android.content.Intent; | ||||
| 10 | import android.content.IntentFilter; | ||||
| 11 | import android.net.wifi.ScanResult; | ||||
| 12 | import android.net.wifi.WifiManager; | ||||
| 13 | import android.os.Handler; | ||||
| 14 | import android.os.IBinder; | ||||
| 15 | import android.os.PowerManager; | ||||
| 16 | import android.os.RemoteCallbackList; | ||||
| 17 | import android.os.RemoteException; | ||||
| 18 | import android.os.Vibrator; | ||||
| 19 | import android.util.Log; | ||||
| 20 | import android.widget.Toast; | ||||
| 21 | |||||
| 22 | import java.io.IOException; | ||||
| 23 | import java.util.Date; | ||||
| 24 | import java.util.List; | ||||
| 25 | import java.util.concurrent.atomic.AtomicBoolean; | ||||
| 26 | |||||
| 27 | import javax.crypto.Mac; | ||||
| 28 | import javax.crypto.SecretKey; | ||||
| 29 | import javax.crypto.spec.SecretKeySpec; | ||||
| 30 | |||||
| 31 | import org.apache.http.HttpResponse; | ||||
| 32 | import org.apache.http.client.ClientProtocolException; | ||||
| 33 | import org.apache.http.client.HttpClient; | ||||
| 34 | import org.apache.http.client.ResponseHandler; | ||||
| 35 | import org.apache.http.client.methods.HttpGet; | ||||
| 36 | import org.apache.http.client.methods.HttpUriRequest; | ||||
| 37 | import org.apache.http.impl.client.DefaultHttpClient; | ||||
| 38 | |||||
| 39 | public class InRangeService extends Service { | ||||
| 40 | |||||
| 41 | private static final String TAG = "InRangeService"; | ||||
| 42 | private static final int NOTIFY_ID_SCANNING = 1; | ||||
| 43 | private static final int NOTIFY_ID_EVENT = 2; | ||||
| 44 | |||||
| 45 | private AtomicBoolean isScanning = new AtomicBoolean(false); | ||||
| 46 | private AtomicBoolean scanTimerOutstanding = new AtomicBoolean(false); | ||||
| 47 | // If we've reached the state that we should open the door. Set to false once we actually do. | ||||
| 48 | private AtomicBoolean shouldOpen = new AtomicBoolean(false); | ||||
| 49 | private AtomicBoolean httpRequestOustanding = new AtomicBoolean(false); | ||||
| 50 | |||||
| 51 | // A message loop handler to post runnables to in the future: | ||||
| 52 | private Handler handler = new Handler(); | ||||
| 53 | |||||
| 54 | private Vibrator vibrator; | ||||
| 55 | private WifiManager.WifiLock wifiLock; | ||||
| 56 | private PowerManager.WakeLock cpuLock; | ||||
| 57 | |||||
| 58 | private final RemoteCallbackList<IGarageScanCallback> callbackList = new RemoteCallbackList<IGarageScanCallback>(); | ||||
| 59 | |||||
| 60 | // We don't trust the first scan result (it seems to be old or cached sometimes), so instead | ||||
| 61 | // we wait for an out-of-range --> in-range transition. This bool keeps track of whether or not | ||||
| 62 | // we've seen an out-of-range scan result since we started scanning. | ||||
| 63 | private AtomicBoolean outOfRangeScanReceived = new AtomicBoolean(false); | ||||
| 64 | |||||
| 65 | // For debugging, this can be false to just do a constant scan without actually opening the garage. | ||||
| 66 | protected boolean debugMode = false; | ||||
| 67 | |||||
| 68 | // For listening to system updates of network & wifi connectivity state: | ||||
| 69 | private IntentFilter networkChangeIntents = new IntentFilter(); | ||||
| 70 | private final BroadcastReceiver onNetworkChange = new BroadcastReceiver() { | ||||
| 71 | public void onReceive(Context context, Intent intent) { | ||||
| 72 | if (shouldOpen.get() && !httpRequestOustanding.get()) { | ||||
| 73 | // Maybe the first HTTP request failed, but now we have connectivity, so | ||||
| 74 | // we should try again. | ||||
| 75 | openGarage(); | ||||
| 76 | } | ||||
| 77 | } | ||||
| 78 | }; | ||||
| 79 | |||||
| 80 | // For listening to wifi scanning update: | ||||
| 81 | private IntentFilter scanResultIntentFilter = new IntentFilter(); | ||||
| 82 | private final BroadcastReceiver onScanResult = new BroadcastReceiver() { | ||||
| 83 | public void onReceive(Context context, Intent intent) { | ||||
| 84 | if (!intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { | ||||
| 85 | Log.d("BOGUS ACTION", null); | ||||
| 86 | return; | ||||
| 87 | } | ||||
| 88 | if (!isScanning.get()) { | ||||
| 89 | // Canceled in the meantime. | ||||
| 90 | return; | ||||
| 91 | } | ||||
| 92 | |||||
| 93 | if (debugMode) { | ||||
| 94 | vibrator.vibrate(50); | ||||
| 95 | } | ||||
| 96 | |||||
| 97 | List<ScanResult> scanResults = wifi().getScanResults(); | ||||
| 98 | Log.v(TAG, "Scan results."); | ||||
| 99 | int privateLevel = -999; | ||||
| 100 | int publicLevel = -999; | ||||
| 101 | StringBuilder sb = new StringBuilder(); | ||||
| 102 | for (ScanResult ap : scanResults) { | ||||
| 103 | Log.v(TAG, | ||||
| 104 | "AP: " + ap.SSID + ", " + ap.capabilities + ", freq=" + ap.frequency + ", " | ||||
| 105 | + "bssid=" + ap.BSSID + ", lev=" + ap.level); | ||||
| 106 | sb.append(ap.SSID + " == " + ap.level + "\n"); | ||||
| 107 | if (ap.SSID.equals("FitzPublic")) { | ||||
| 108 | publicLevel = ap.level; | ||||
| 109 | } else if (ap.SSID.equals("FitzPrivate")) { | ||||
| 110 | privateLevel = ap.level; | ||||
| 111 | } | ||||
| 112 | } | ||||
| 113 | |||||
| 114 | boolean inRange = (privateLevel != -999 || publicLevel != -999); | ||||
| 115 | if (!inRange) { | ||||
| 116 | outOfRangeScanReceived.set(true); | ||||
| 117 | } | ||||
| 118 | sendScanToClients(sb.toString()); | ||||
| 119 | |||||
| 120 | if (!debugMode && inRange && outOfRangeScanReceived.get()) { | ||||
| 121 | stopScanningAndOpenGarage(); | ||||
| 122 | } else if (isScanning.get()) { | ||||
| 123 | // start scanning again in a second, if a timer's not already outstanding | ||||
| 124 | if (scanTimerOutstanding.compareAndSet(false, true)) { | ||||
| 125 | handler.postDelayed(new Runnable() { | ||||
| 126 | public void run() { | ||||
| 127 | scanTimerOutstanding.set(false); | ||||
| 128 | Log.d(TAG, "Starting a wifi scan..."); | ||||
| 129 | wifi().startScan(); | ||||
| 130 | } | ||||
| 131 | }, 1000); | ||||
| 132 | } | ||||
| 133 | } | ||||
| 134 | } | ||||
| 135 | }; | ||||
| 136 | |||||
| 137 | @Override | ||||
| 138 | public IBinder onBind(Intent arg0) { | ||||
| 139 | return scanService; | ||||
| 140 | } | ||||
| 141 | |||||
| 142 | public void onDestroy() { | ||||
| 143 | stopScanning(); | ||||
| 144 | callbackList.kill(); // unregister all callbacks | ||||
| 145 | super.onDestroy(); | ||||
| 146 | } | ||||
| 147 | |||||
| 148 | @Override | ||||
| 149 | public void onCreate() { | ||||
| 150 | super.onCreate(); | ||||
| 151 | wifiLock = wifi().createWifiLock("GarageWifiLock"); | ||||
| 152 | vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); | ||||
| 153 | |||||
| 154 | networkChangeIntents.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); | ||||
| 155 | networkChangeIntents.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); | ||||
| 156 | |||||
| 157 | scanResultIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); | ||||
| 158 | |||||
| 159 | PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); | ||||
| 160 | cpuLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Garage CPU lock"); | ||||
| 161 | |||||
| 162 | Log.d(TAG, "onCreate"); | ||||
| 163 | } | ||||
| 164 | |||||
| 165 | protected void stopScanningAndOpenGarage() { | ||||
| 166 | stopScanning(); | ||||
| 167 | doNotification("Garage In Range", "Starting to open."); | ||||
| 168 | shouldOpen.set(true); | ||||
| 169 | openGarage(); | ||||
| 170 | vibrator.vibrate(2000); // 2 seconds | ||||
| 171 | } | ||||
| 172 | |||||
| 173 | private void doNotification(String title, String text) { | ||||
| 174 | Notification n = new Notification(R.drawable.icon, title, System.currentTimeMillis()); | ||||
| 175 | PendingIntent pIntent = PendingIntent.getActivity(this, 0, | ||||
| 176 | new Intent(this, GarageDoorActivity.class), 0); | ||||
| 177 | n.setLatestEventInfo(this, title, text, pIntent); | ||||
| 178 | NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); | ||||
| 179 | nm.notify(NOTIFY_ID_EVENT, n); | ||||
| 180 | } | ||||
| 181 | |||||
| 182 | private void notifyError(String string) { | ||||
| 183 | doNotification("Garage Error", string); | ||||
| 184 | logToClients(string); | ||||
| 185 | } | ||||
| 186 | |||||
| 187 | // Returns true if the HTTP request was started. | ||||
| 188 | private boolean openGarage() { | ||||
| 189 | if (!shouldOpen.get() ) { | ||||
| 190 | Log.e(TAG, "openGarage() called but shouldOpen isn't true"); | ||||
| 191 | return false; | ||||
| 192 | } | ||||
| 193 | if (!httpRequestOustanding.compareAndSet(false, true)) { | ||||
| 194 | Log.d(TAG, "Not opening garage door due to other outstanding HTTP request."); | ||||
| 195 | return false; | ||||
| 196 | } | ||||
| 197 | final String urlBase = getString(R.string.garage_url); | ||||
| 198 | final HttpClient client = new DefaultHttpClient(); | ||||
| 199 | Date now = new Date(); | ||||
| 200 | long epochTime = now.getTime() / 1000; | ||||
| 201 | String url = urlBase + "?t=" + epochTime + "&key=" + hmacSha1(""+epochTime, getString(R.string.shared_key)); | ||||
| 202 | |||||
| 203 | Log.d(TAG, "Attempting open of: " + urlBase); | ||||
| 204 | logToClients("Sending HTTP request to " + url); | ||||
| 205 | |||||
| 206 | final HttpUriRequest request = new HttpGet(url); | ||||
| 207 | Runnable httpRunnable = new Runnable() { | ||||
| 208 | public void run() { | ||||
| 209 | try { | ||||
| 210 | client.execute(request, new ResponseHandler<HttpResponse>() { | ||||
| 211 | public HttpResponse handleResponse(HttpResponse response) throws ClientProtocolException, IOException { | ||||
| 212 | if (response.getStatusLine().getStatusCode() == 200) { | ||||
| 213 | doNotification("Garage Opened", "The garage door was opened."); | ||||
| 214 | logToClients("HTTP success. Opened."); | ||||
| 215 | shouldOpen.set(false); // done. | ||||
| 216 | } else { | ||||
| 217 | notifyError("HTTP error: " + response.toString()); | ||||
| 218 | } | ||||
| 219 | httpRequestOustanding.set(false); | ||||
| 220 | return response; | ||||
| 221 | } | ||||
| 222 | |||||
| 223 | }); | ||||
| 224 | } catch (ClientProtocolException e) { | ||||
| 225 | notifyError("ClientProtocolException = " + e); | ||||
| 226 | e.printStackTrace(); | ||||
| 227 | retryOpenGarageSoon(); | ||||
| 228 | } catch (IOException e) { | ||||
| 229 | notifyError("IOException = " + e); | ||||
| 230 | e.printStackTrace(); | ||||
| 231 | retryOpenGarageSoon(); | ||||
| 232 | } finally { | ||||
| 233 | httpRequestOustanding.set(false); | ||||
| 234 | } | ||||
| 235 | } | ||||
| 236 | }; | ||||
| 237 | Thread httpThread = new Thread(httpRunnable); | ||||
| 238 | httpThread.start(); | ||||
| 239 | return true; | ||||
| 240 | } | ||||
| 241 | |||||
| 242 | protected void retryOpenGarageSoon() { | ||||
| 243 | Log.d(TAG, "Retrying garage door open in 1 second..."); | ||||
| 244 | handler.postDelayed(new Runnable() { | ||||
| 245 | public void run() { | ||||
| 246 | openGarage(); | ||||
| 247 | } | ||||
| 248 | }, 1000); | ||||
| 249 | } | ||||
| 250 | |||||
| 251 | @Override | ||||
| 252 | public void onStart(Intent intent, int startId) { | ||||
| 253 | super.onStart(intent, startId); | ||||
| 254 | Log.d(TAG, "onStart"); | ||||
| 255 | startScanning(); | ||||
| 256 | } | ||||
| 257 | |||||
| 258 | private void startScanning() { | ||||
| 259 | Log.d(TAG, "startScanning()"); | ||||
| 260 | if (!isScanning.compareAndSet(false, true)) { | ||||
| 261 | logToClients("Scanning already running."); | ||||
| 262 | return; | ||||
| 263 | } | ||||
| 264 | |||||
| 265 | cpuLock.acquire(); | ||||
| 266 | logToClients("Garage wifi scan starting."); | ||||
| 267 | |||||
| 268 | outOfRangeScanReceived.set(false); | ||||
| 269 | shouldOpen.set(false); | ||||
| 270 | |||||
| 271 | setForeground(true); // don't swap this out. | ||||
| 272 | |||||
| 273 | Toast.makeText(this, "Garage Scan Started", Toast.LENGTH_SHORT).show(); | ||||
| 274 | |||||
| 275 | Notification n = new Notification(); | ||||
| 276 | n.icon = R.drawable.icon; | ||||
| 277 | n.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; | ||||
| 278 | n.setLatestEventInfo(this, "Garage Scanning", "Garage door wifi scan is in progress.", | ||||
| 279 | PendingIntent.getActivity(this, 0, | ||||
| 280 | new Intent(this, GarageDoorActivity.class), 0)); | ||||
| 281 | notificationManager().cancel(NOTIFY_ID_EVENT); | ||||
| 282 | notificationManager().notify(NOTIFY_ID_SCANNING, n); | ||||
| 283 | |||||
| 284 | wifiLock.acquire(); | ||||
| 285 | registerReceiver(onScanResult, scanResultIntentFilter); | ||||
| 286 | wifi().startScan(); | ||||
| 287 | } | ||||
| 288 | |||||
| 289 | private NotificationManager notificationManager() { | ||||
| 290 | return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); | ||||
| 291 | } | ||||
| 292 | |||||
| 293 | private void stopScanning() { | ||||
| 294 | Log.d(TAG, "stopScanning()"); | ||||
| 295 | if (!isScanning.compareAndSet(true, false)) { | ||||
| 296 | logToClients("Scanning was already stopped."); | ||||
| 297 | return; | ||||
| 298 | } | ||||
| 299 | cpuLock.release(); | ||||
| 300 | setForeground(false); | ||||
| 301 | logToClients("Stopping scanning."); | ||||
| 302 | wifiLock.release(); | ||||
| 303 | unregisterReceiver(onScanResult); | ||||
| 304 | notificationManager().cancel(NOTIFY_ID_SCANNING); | ||||
| 305 | } | ||||
| 306 | |||||
| 307 | private synchronized void logToClients(String string) { | ||||
| 308 | // Broadcast to all clients the new value. | ||||
| 309 | final int N = callbackList.beginBroadcast(); | ||||
| 310 | for (int i = 0; i < N; ++i) { | ||||
| 311 | try { | ||||
| 312 | callbackList.getBroadcastItem(i).logToClient(string); | ||||
| 313 | } catch (RemoteException e) { | ||||
| 314 | // The RemoteCallbackList will take care of removing | ||||
| 315 | // the dead object for us. | ||||
| 316 | } | ||||
| 317 | } | ||||
| 318 | callbackList.finishBroadcast(); | ||||
| 319 | } | ||||
| 320 | |||||
| 321 | private synchronized void sendScanToClients(String string) { | ||||
| 322 | // Broadcast to all clients the new value. | ||||
| 323 | final int N = callbackList.beginBroadcast(); | ||||
| 324 | for (int i = 0; i < N; ++i) { | ||||
| 325 | try { | ||||
| 326 | callbackList.getBroadcastItem(i).onScanResults(string); | ||||
| 327 | } catch (RemoteException e) { | ||||
| 328 | // The RemoteCallbackList will take care of removing | ||||
| 329 | // the dead object for us. | ||||
| 330 | } | ||||
| 331 | } | ||||
| 332 | callbackList.finishBroadcast(); | ||||
| 333 | } | ||||
| 334 | |||||
| 335 | private WifiManager wifi() { | ||||
| 336 | return (WifiManager) getSystemService(Context.WIFI_SERVICE); | ||||
| 337 | } | ||||
| 338 | |||||
| 339 | private static final char[] HEX_CHAR = { | ||||
| 340 | '0', '1', '2', '3', '4', '5', '6', '7', | ||||
| 341 | '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' | ||||
| 342 | }; | ||||
| 343 | |||||
| 344 | public static String hmacSha1(String plainText, String keyString) | ||||
| 345 | { | ||||
| 346 | try { | ||||
| 347 | SecretKey key = new SecretKeySpec(keyString.getBytes(), "HmacSHA1"); | ||||
| 348 | Mac mac = Mac.getInstance("HmacSHA1"); | ||||
| 349 | mac.init(key); | ||||
| 350 | mac.update(plainText.getBytes()); | ||||
| 351 | byte[] digest = mac.doFinal(); | ||||
| 352 | char[] hexDigest = new char[40]; | ||||
| 353 | for (int i = 0; i < 20; ++i) { | ||||
| 354 | int byteValue = 0xFF & digest[i]; // signed to unsigned. Java, man. | ||||
| 355 | hexDigest[i * 2] = HEX_CHAR[byteValue >> 4]; | ||||
| 356 | hexDigest[i * 2 + 1] = HEX_CHAR[byteValue & 0xf]; | ||||
| 357 | } | ||||
| 358 | return new String(hexDigest); | ||||
| 359 | } catch (Exception e) { | ||||
| 360 | e.printStackTrace(); | ||||
| 361 | return null; | ||||
| 362 | } | ||||
| 363 | } | ||||
| 364 | |||||
| 365 | private final IGarageScanService.Stub scanService = new IGarageScanService.Stub() { | ||||
| 366 | |||||
| 367 | public boolean isScanning() throws RemoteException { | ||||
| 368 | return isScanning.get(); | ||||
| 369 | } | ||||
| 370 | |||||
| 371 | public void setScanning(boolean isEnabled) throws RemoteException { | ||||
| 372 | if (isEnabled) { | ||||
| 373 | startScanning(); | ||||
| 374 | } else { | ||||
| 375 | stopScanning(); | ||||
| 376 | } | ||||
| 377 | } | ||||
| 378 | |||||
| 379 | public void registerCallback(IGarageScanCallback callback) | ||||
| 380 | throws RemoteException { | ||||
| 381 | if (callback != null) { | ||||
| 382 | callbackList.register(callback); | ||||
| 383 | } | ||||
| 384 | } | ||||
| 385 | |||||
| 386 | public void unregisterCallback(IGarageScanCallback callback) | ||||
| 387 | throws RemoteException { | ||||
| 388 | if (callback != null) { | ||||
| 389 | callbackList.unregister(callback); | ||||
| 390 | } | ||||
| 391 | } | ||||
| 392 | |||||
| 393 | public void setDebugMode(boolean debugMode) throws RemoteException { | ||||
| 394 | InRangeService.this.debugMode = debugMode; | ||||
| 395 | } | ||||
| 396 | |||||
| 397 | public void openGarageNow() throws RemoteException { | ||||
| 398 | shouldOpen.set(true); | ||||
| 399 | if (openGarage()) { | ||||
| 400 | logToClients("HTTP request sent to open garage."); | ||||
| 401 | } else { | ||||
| 402 | logToClients("Didn't send garage open request."); | ||||
| 403 | } | ||||
| 404 | } | ||||
| 405 | }; | ||||
| 406 | } | ||||
