blob: 264e2e9c3b1570a3043fb73502088b795330d8fb [file] [log] [blame]
Jeff Brown96307042012-07-27 15:51:34 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.power;
18
Dianne Hackborn713df152013-05-17 11:27:57 -070019import android.app.AppOpsManager;
20import com.android.internal.app.IAppOpsService;
Jeff Brown96307042012-07-27 15:51:34 -070021import com.android.internal.app.IBatteryStats;
22import com.android.server.EventLogTags;
23
24import android.app.ActivityManagerNative;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
Jeff Brown84e27562012-12-07 13:56:34 -080028import android.media.AudioManager;
29import android.media.Ringtone;
30import android.media.RingtoneManager;
31import android.net.Uri;
Jeff Brown96307042012-07-27 15:51:34 -070032import android.os.BatteryStats;
33import android.os.Handler;
34import android.os.Looper;
35import android.os.Message;
36import android.os.PowerManager;
Jeff Brown96307042012-07-27 15:51:34 -070037import android.os.RemoteException;
38import android.os.SystemClock;
Dianne Hackborn5ac72a22012-08-29 18:32:08 -070039import android.os.UserHandle;
Jeff Brown96307042012-07-27 15:51:34 -070040import android.os.WorkSource;
Jeff Brown84e27562012-12-07 13:56:34 -080041import android.provider.Settings;
Jeff Brown96307042012-07-27 15:51:34 -070042import android.util.EventLog;
43import android.util.Slog;
44import android.view.WindowManagerPolicy;
Jeff Brown96307042012-07-27 15:51:34 -070045
46/**
47 * Sends broadcasts about important power state changes.
Jeff Brown54308352012-10-04 17:59:58 -070048 * <p>
Jeff Brown96307042012-07-27 15:51:34 -070049 * This methods of this class may be called by the power manager service while
50 * its lock is being held. Internally it takes care of sending broadcasts to
51 * notify other components of the system or applications asynchronously.
Jeff Brown54308352012-10-04 17:59:58 -070052 * </p><p>
Jeff Brown96307042012-07-27 15:51:34 -070053 * The notifier is designed to collapse unnecessary broadcasts when it is not
54 * possible for the system to have observed an intermediate state.
Jeff Brown54308352012-10-04 17:59:58 -070055 * </p><p>
56 * For example, if the device wakes up, goes to sleep, wakes up again and goes to
57 * sleep again before the wake up notification is sent, then the system will
58 * be told about only one wake up and sleep. However, we always notify the
59 * fact that at least one transition occurred. It is especially important to
60 * tell the system when we go to sleep so that it can lock the keyguard if needed.
61 * </p>
Jeff Brown96307042012-07-27 15:51:34 -070062 */
63final class Notifier {
64 private static final String TAG = "PowerManagerNotifier";
65
66 private static final boolean DEBUG = false;
67
68 private static final int POWER_STATE_UNKNOWN = 0;
69 private static final int POWER_STATE_AWAKE = 1;
70 private static final int POWER_STATE_ASLEEP = 2;
71
72 private static final int MSG_USER_ACTIVITY = 1;
73 private static final int MSG_BROADCAST = 2;
Jeff Brown84e27562012-12-07 13:56:34 -080074 private static final int MSG_WIRELESS_CHARGING_STARTED = 3;
Jeff Brown96307042012-07-27 15:51:34 -070075
76 private final Object mLock = new Object();
77
78 private final Context mContext;
79 private final IBatteryStats mBatteryStats;
Dianne Hackborn713df152013-05-17 11:27:57 -070080 private final IAppOpsService mAppOps;
Jeff Brown96307042012-07-27 15:51:34 -070081 private final SuspendBlocker mSuspendBlocker;
Jeff Brownc38c9be2012-10-04 13:16:19 -070082 private final ScreenOnBlocker mScreenOnBlocker;
Jeff Brown96307042012-07-27 15:51:34 -070083 private final WindowManagerPolicy mPolicy;
Jeff Brown96307042012-07-27 15:51:34 -070084
85 private final NotifierHandler mHandler;
86 private final Intent mScreenOnIntent;
87 private final Intent mScreenOffIntent;
88
89 // The current power state.
90 private int mActualPowerState;
91 private int mLastGoToSleepReason;
92
Jeff Brown54308352012-10-04 17:59:58 -070093 // True if there is a pending transition that needs to be reported.
94 private boolean mPendingWakeUpBroadcast;
95 private boolean mPendingGoToSleepBroadcast;
96
Jeff Brown96307042012-07-27 15:51:34 -070097 // The currently broadcasted power state. This reflects what other parts of the
98 // system have observed.
99 private int mBroadcastedPowerState;
100 private boolean mBroadcastInProgress;
101 private long mBroadcastStartTime;
102
103 // True if a user activity message should be sent.
104 private boolean mUserActivityPending;
105
Jeff Brownc38c9be2012-10-04 13:16:19 -0700106 // True if the screen on blocker has been acquired.
107 private boolean mScreenOnBlockerAcquired;
108
Jeff Brown96307042012-07-27 15:51:34 -0700109 public Notifier(Looper looper, Context context, IBatteryStats batteryStats,
Dianne Hackborn713df152013-05-17 11:27:57 -0700110 IAppOpsService appOps, SuspendBlocker suspendBlocker, ScreenOnBlocker screenOnBlocker,
Jeff Brownc38c9be2012-10-04 13:16:19 -0700111 WindowManagerPolicy policy) {
Jeff Brown96307042012-07-27 15:51:34 -0700112 mContext = context;
113 mBatteryStats = batteryStats;
Dianne Hackborn713df152013-05-17 11:27:57 -0700114 mAppOps = appOps;
Jeff Brown96307042012-07-27 15:51:34 -0700115 mSuspendBlocker = suspendBlocker;
Jeff Brownc38c9be2012-10-04 13:16:19 -0700116 mScreenOnBlocker = screenOnBlocker;
Jeff Brown96307042012-07-27 15:51:34 -0700117 mPolicy = policy;
Jeff Brown96307042012-07-27 15:51:34 -0700118
119 mHandler = new NotifierHandler(looper);
120 mScreenOnIntent = new Intent(Intent.ACTION_SCREEN_ON);
121 mScreenOnIntent.addFlags(
122 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND);
123 mScreenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF);
124 mScreenOffIntent.addFlags(
125 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND);
126 }
127
128 /**
129 * Called when a wake lock is acquired.
130 */
Dianne Hackborn713df152013-05-17 11:27:57 -0700131 public void onWakeLockAcquired(int flags, String tag, String packageName,
132 int ownerUid, int ownerPid, WorkSource workSource) {
Jeff Brown96307042012-07-27 15:51:34 -0700133 if (DEBUG) {
134 Slog.d(TAG, "onWakeLockAcquired: flags=" + flags + ", tag=\"" + tag
Dianne Hackborn713df152013-05-17 11:27:57 -0700135 + "\", packageName=" + packageName
136 + ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
Jeff Brown96307042012-07-27 15:51:34 -0700137 + ", workSource=" + workSource);
138 }
139
Craig Mautner259328c2012-08-21 19:30:58 -0700140 try {
141 final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
142 if (workSource != null) {
143 mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag, monitorType);
144 } else {
145 mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, monitorType);
Dianne Hackborn713df152013-05-17 11:27:57 -0700146 // XXX need to deal with disabled operations.
Dianne Hackborne98f5db2013-07-17 17:23:25 -0700147 mAppOps.startOperation(AppOpsManager.getToken(mAppOps),
148 AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
Jeff Brown96307042012-07-27 15:51:34 -0700149 }
Craig Mautner259328c2012-08-21 19:30:58 -0700150 } catch (RemoteException ex) {
151 // Ignore
Jeff Brown96307042012-07-27 15:51:34 -0700152 }
153 }
154
155 /**
156 * Called when a wake lock is released.
157 */
Dianne Hackborn713df152013-05-17 11:27:57 -0700158 public void onWakeLockReleased(int flags, String tag, String packageName,
159 int ownerUid, int ownerPid, WorkSource workSource) {
Jeff Brown96307042012-07-27 15:51:34 -0700160 if (DEBUG) {
161 Slog.d(TAG, "onWakeLockReleased: flags=" + flags + ", tag=\"" + tag
Dianne Hackborn713df152013-05-17 11:27:57 -0700162 + "\", packageName=" + packageName
163 + ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
Jeff Brown96307042012-07-27 15:51:34 -0700164 + ", workSource=" + workSource);
165 }
166
Craig Mautner259328c2012-08-21 19:30:58 -0700167 try {
168 final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
169 if (workSource != null) {
170 mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag, monitorType);
171 } else {
172 mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag, monitorType);
Dianne Hackborne98f5db2013-07-17 17:23:25 -0700173 mAppOps.finishOperation(AppOpsManager.getToken(mAppOps),
174 AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
Jeff Brown96307042012-07-27 15:51:34 -0700175 }
Craig Mautner259328c2012-08-21 19:30:58 -0700176 } catch (RemoteException ex) {
177 // Ignore
Jeff Brown96307042012-07-27 15:51:34 -0700178 }
179 }
180
Jeff Brown96307042012-07-27 15:51:34 -0700181 private static int getBatteryStatsWakeLockMonitorType(int flags) {
182 switch (flags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
183 case PowerManager.PARTIAL_WAKE_LOCK:
184 case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
185 return BatteryStats.WAKE_TYPE_PARTIAL;
186 default:
187 return BatteryStats.WAKE_TYPE_FULL;
188 }
189 }
190
191 /**
192 * Called when the screen is turned on.
193 */
194 public void onScreenOn() {
195 if (DEBUG) {
196 Slog.d(TAG, "onScreenOn");
197 }
198
199 try {
200 mBatteryStats.noteScreenOn();
201 } catch (RemoteException ex) {
202 // Ignore
203 }
204 }
205
206 /**
207 * Called when the screen is turned off.
208 */
209 public void onScreenOff() {
210 if (DEBUG) {
211 Slog.d(TAG, "onScreenOff");
212 }
213
214 try {
215 mBatteryStats.noteScreenOff();
216 } catch (RemoteException ex) {
217 // Ignore
218 }
219 }
220
221 /**
222 * Called when the screen changes brightness.
223 */
224 public void onScreenBrightness(int brightness) {
225 if (DEBUG) {
226 Slog.d(TAG, "onScreenBrightness: brightness=" + brightness);
227 }
228
229 try {
230 mBatteryStats.noteScreenBrightness(brightness);
231 } catch (RemoteException ex) {
232 // Ignore
233 }
234 }
235
236 /**
237 * Called when the device is waking up from sleep and the
238 * display is about to be turned on.
239 */
240 public void onWakeUpStarted() {
241 if (DEBUG) {
242 Slog.d(TAG, "onWakeUpStarted");
243 }
244
245 synchronized (mLock) {
246 if (mActualPowerState != POWER_STATE_AWAKE) {
247 mActualPowerState = POWER_STATE_AWAKE;
Jeff Brown54308352012-10-04 17:59:58 -0700248 mPendingWakeUpBroadcast = true;
Jeff Brownc38c9be2012-10-04 13:16:19 -0700249 if (!mScreenOnBlockerAcquired) {
250 mScreenOnBlockerAcquired = true;
251 mScreenOnBlocker.acquire();
252 }
Jeff Brown96307042012-07-27 15:51:34 -0700253 updatePendingBroadcastLocked();
254 }
255 }
256 }
257
258 /**
259 * Called when the device has finished waking up from sleep
260 * and the display has been turned on.
261 */
262 public void onWakeUpFinished() {
263 if (DEBUG) {
264 Slog.d(TAG, "onWakeUpFinished");
265 }
266 }
267
268 /**
269 * Called when the device is going to sleep.
270 */
271 public void onGoToSleepStarted(int reason) {
272 if (DEBUG) {
273 Slog.d(TAG, "onGoToSleepStarted");
274 }
275
276 synchronized (mLock) {
277 mLastGoToSleepReason = reason;
278 }
279 }
280
281 /**
282 * Called when the device has finished going to sleep and the
283 * display has been turned off.
284 *
285 * This is a good time to make transitions that we don't want the user to see,
286 * such as bringing the key guard to focus. There's no guarantee for this,
287 * however because the user could turn the device on again at any time.
288 * Some things may need to be protected by other mechanisms that defer screen on.
289 */
290 public void onGoToSleepFinished() {
291 if (DEBUG) {
292 Slog.d(TAG, "onGoToSleepFinished");
293 }
294
295 synchronized (mLock) {
296 if (mActualPowerState != POWER_STATE_ASLEEP) {
297 mActualPowerState = POWER_STATE_ASLEEP;
Jeff Brown54308352012-10-04 17:59:58 -0700298 mPendingGoToSleepBroadcast = true;
Jeff Brown96307042012-07-27 15:51:34 -0700299 if (mUserActivityPending) {
300 mUserActivityPending = false;
301 mHandler.removeMessages(MSG_USER_ACTIVITY);
302 }
303 updatePendingBroadcastLocked();
304 }
305 }
306 }
307
308 /**
309 * Called when there has been user activity.
310 */
311 public void onUserActivity(int event, int uid) {
312 if (DEBUG) {
313 Slog.d(TAG, "onUserActivity: event=" + event + ", uid=" + uid);
314 }
315
316 try {
317 mBatteryStats.noteUserActivity(uid, event);
318 } catch (RemoteException ex) {
319 // Ignore
320 }
321
322 synchronized (mLock) {
323 if (!mUserActivityPending) {
324 mUserActivityPending = true;
325 Message msg = mHandler.obtainMessage(MSG_USER_ACTIVITY);
326 msg.setAsynchronous(true);
327 mHandler.sendMessage(msg);
328 }
329 }
330 }
331
Jeff Brown84e27562012-12-07 13:56:34 -0800332 /**
333 * Called when wireless charging has started so as to provide user feedback.
334 */
335 public void onWirelessChargingStarted() {
336 if (DEBUG) {
337 Slog.d(TAG, "onWirelessChargingStarted");
338 }
339
340 mSuspendBlocker.acquire();
341 Message msg = mHandler.obtainMessage(MSG_WIRELESS_CHARGING_STARTED);
342 msg.setAsynchronous(true);
343 mHandler.sendMessage(msg);
344 }
345
Jeff Brown96307042012-07-27 15:51:34 -0700346 private void updatePendingBroadcastLocked() {
347 if (!mBroadcastInProgress
348 && mActualPowerState != POWER_STATE_UNKNOWN
Jeff Brown54308352012-10-04 17:59:58 -0700349 && (mPendingWakeUpBroadcast || mPendingGoToSleepBroadcast
350 || mActualPowerState != mBroadcastedPowerState)) {
Jeff Brown96307042012-07-27 15:51:34 -0700351 mBroadcastInProgress = true;
352 mSuspendBlocker.acquire();
353 Message msg = mHandler.obtainMessage(MSG_BROADCAST);
354 msg.setAsynchronous(true);
355 mHandler.sendMessage(msg);
356 }
357 }
358
Jeff Brown54308352012-10-04 17:59:58 -0700359 private void finishPendingBroadcastLocked() {
360 mBroadcastInProgress = false;
361 mSuspendBlocker.release();
362 }
363
Jeff Brown96307042012-07-27 15:51:34 -0700364 private void sendUserActivity() {
365 synchronized (mLock) {
366 if (!mUserActivityPending) {
367 return;
368 }
369 mUserActivityPending = false;
370 }
371
372 mPolicy.userActivity();
373 }
374
375 private void sendNextBroadcast() {
376 final int powerState;
377 final int goToSleepReason;
378 synchronized (mLock) {
Jeff Brown54308352012-10-04 17:59:58 -0700379 if (mBroadcastedPowerState == POWER_STATE_UNKNOWN) {
380 // Broadcasted power state is unknown. Send wake up.
381 mPendingWakeUpBroadcast = false;
382 mBroadcastedPowerState = POWER_STATE_AWAKE;
383 } else if (mBroadcastedPowerState == POWER_STATE_AWAKE) {
384 // Broadcasted power state is awake. Send asleep if needed.
385 if (mPendingWakeUpBroadcast || mPendingGoToSleepBroadcast
386 || mActualPowerState == POWER_STATE_ASLEEP) {
387 mPendingGoToSleepBroadcast = false;
388 mBroadcastedPowerState = POWER_STATE_ASLEEP;
389 } else {
390 finishPendingBroadcastLocked();
391 return;
392 }
393 } else {
394 // Broadcasted power state is asleep. Send awake if needed.
395 if (mPendingWakeUpBroadcast || mPendingGoToSleepBroadcast
396 || mActualPowerState == POWER_STATE_AWAKE) {
397 mPendingWakeUpBroadcast = false;
398 mBroadcastedPowerState = POWER_STATE_AWAKE;
399 } else {
400 finishPendingBroadcastLocked();
401 return;
402 }
Jeff Brown96307042012-07-27 15:51:34 -0700403 }
404
Jeff Brown96307042012-07-27 15:51:34 -0700405 mBroadcastStartTime = SystemClock.uptimeMillis();
Jeff Brown54308352012-10-04 17:59:58 -0700406 powerState = mBroadcastedPowerState;
407 goToSleepReason = mLastGoToSleepReason;
Jeff Brown96307042012-07-27 15:51:34 -0700408 }
409
410 EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_SEND, 1);
411
412 if (powerState == POWER_STATE_AWAKE) {
413 sendWakeUpBroadcast();
414 } else {
415 sendGoToSleepBroadcast(goToSleepReason);
416 }
417 }
418
419 private void sendWakeUpBroadcast() {
420 if (DEBUG) {
421 Slog.d(TAG, "Sending wake up broadcast.");
422 }
423
424 EventLog.writeEvent(EventLogTags.POWER_SCREEN_STATE, 1, 0, 0, 0);
425
426 mPolicy.screenTurningOn(mScreenOnListener);
Jeff Brownc38c9be2012-10-04 13:16:19 -0700427
Jeff Brown96307042012-07-27 15:51:34 -0700428 try {
429 ActivityManagerNative.getDefault().wakingUp();
430 } catch (RemoteException e) {
431 // ignore it
432 }
433
434 if (ActivityManagerNative.isSystemReady()) {
Dianne Hackborn5ac72a22012-08-29 18:32:08 -0700435 mContext.sendOrderedBroadcastAsUser(mScreenOnIntent, UserHandle.ALL, null,
Jeff Brown96307042012-07-27 15:51:34 -0700436 mWakeUpBroadcastDone, mHandler, 0, null, null);
437 } else {
438 EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
439 sendNextBroadcast();
440 }
441 }
442
Jeff Brownc38c9be2012-10-04 13:16:19 -0700443 private final WindowManagerPolicy.ScreenOnListener mScreenOnListener =
444 new WindowManagerPolicy.ScreenOnListener() {
445 @Override
446 public void onScreenOn() {
447 synchronized (mLock) {
448 if (mScreenOnBlockerAcquired && !mPendingWakeUpBroadcast) {
449 mScreenOnBlockerAcquired = false;
450 mScreenOnBlocker.release();
451 }
452 }
453 }
454 };
455
Jeff Brown96307042012-07-27 15:51:34 -0700456 private final BroadcastReceiver mWakeUpBroadcastDone = new BroadcastReceiver() {
457 @Override
458 public void onReceive(Context context, Intent intent) {
459 EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 1,
460 SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
461 sendNextBroadcast();
462 }
463 };
464
465 private void sendGoToSleepBroadcast(int reason) {
466 if (DEBUG) {
467 Slog.d(TAG, "Sending go to sleep broadcast.");
468 }
469
470 int why = WindowManagerPolicy.OFF_BECAUSE_OF_USER;
471 switch (reason) {
472 case PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN:
473 why = WindowManagerPolicy.OFF_BECAUSE_OF_ADMIN;
474 break;
475 case PowerManager.GO_TO_SLEEP_REASON_TIMEOUT:
476 why = WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT;
477 break;
478 }
479
480 EventLog.writeEvent(EventLogTags.POWER_SCREEN_STATE, 0, why, 0, 0);
481
482 mPolicy.screenTurnedOff(why);
483 try {
484 ActivityManagerNative.getDefault().goingToSleep();
485 } catch (RemoteException e) {
486 // ignore it.
487 }
488
489 if (ActivityManagerNative.isSystemReady()) {
Dianne Hackborn5ac72a22012-08-29 18:32:08 -0700490 mContext.sendOrderedBroadcastAsUser(mScreenOffIntent, UserHandle.ALL, null,
Jeff Brown96307042012-07-27 15:51:34 -0700491 mGoToSleepBroadcastDone, mHandler, 0, null, null);
492 } else {
493 EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
494 sendNextBroadcast();
495 }
496 }
497
498 private final BroadcastReceiver mGoToSleepBroadcastDone = new BroadcastReceiver() {
499 @Override
500 public void onReceive(Context context, Intent intent) {
501 EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 0,
502 SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
503 sendNextBroadcast();
504 }
505 };
506
Jeff Brown84e27562012-12-07 13:56:34 -0800507 private void playWirelessChargingStartedSound() {
508 final String soundPath = Settings.Global.getString(mContext.getContentResolver(),
509 Settings.Global.WIRELESS_CHARGING_STARTED_SOUND);
510 if (soundPath != null) {
511 final Uri soundUri = Uri.parse("file://" + soundPath);
512 if (soundUri != null) {
513 final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri);
514 if (sfx != null) {
515 sfx.setStreamType(AudioManager.STREAM_SYSTEM);
516 sfx.play();
517 }
518 }
519 }
520
521 mSuspendBlocker.release();
522 }
523
Jeff Brown96307042012-07-27 15:51:34 -0700524 private final class NotifierHandler extends Handler {
525 public NotifierHandler(Looper looper) {
Jeff Browna2910d02012-08-25 12:29:46 -0700526 super(looper, null, true /*async*/);
Jeff Brown96307042012-07-27 15:51:34 -0700527 }
528
529 @Override
530 public void handleMessage(Message msg) {
531 switch (msg.what) {
532 case MSG_USER_ACTIVITY:
533 sendUserActivity();
534 break;
535
536 case MSG_BROADCAST:
537 sendNextBroadcast();
538 break;
Jeff Brown84e27562012-12-07 13:56:34 -0800539
540 case MSG_WIRELESS_CHARGING_STARTED:
541 playWirelessChargingStartedSound();
542 break;
Jeff Brown96307042012-07-27 15:51:34 -0700543 }
544 }
545 }
546}