bradfitz / android-garage-opener

Android Garage Door Opener

This URL has Read+Write access

Brad Fitzpatrick (author)
Sun Nov 16 11:34:32 -0800 2008
android-garage-opener / src / com / danga / garagedoor / InRangeService.java
eda56259 » Brad Fitzpatrick 2008-11-16 initial [public] commit. 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 }