blob: 4e9c06761e548bad4355a53c30f92f0a53dc151d [file] [log] [blame]
Nick Pellye0fd6932012-07-11 10:26:13 -07001/*
Victoria Lease4cd0a502012-11-02 16:24:08 -07002 * Copyright (C) 2012 The Android Open Source Project
Nick Pellye0fd6932012-07-11 10:26:13 -07003 *
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.location;
18
Philip P. Moltmannbc8b48a2019-09-27 17:06:25 -070019import android.annotation.NonNull;
Philip P. Moltmann6c7377c2019-09-27 17:06:25 -070020import android.annotation.Nullable;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070021import android.app.ActivityManager;
Dianne Hackborn5e45ee62013-01-24 19:13:44 -080022import android.app.AppOpsManager;
Nick Pellye0fd6932012-07-11 10:26:13 -070023import android.app.PendingIntent;
24import android.content.Context;
25import android.content.Intent;
Nick Pelly6fa9ad42012-07-16 12:18:23 -070026import android.location.Geofence;
Nick Pellye0fd6932012-07-11 10:26:13 -070027import android.location.Location;
28import android.location.LocationListener;
29import android.location.LocationManager;
Nick Pelly6fa9ad42012-07-16 12:18:23 -070030import android.location.LocationRequest;
Nick Pellye0fd6932012-07-11 10:26:13 -070031import android.os.Bundle;
Victoria Lease4cd0a502012-11-02 16:24:08 -070032import android.os.Handler;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070033import android.os.Looper;
Victoria Lease4cd0a502012-11-02 16:24:08 -070034import android.os.Message;
Nick Pellye0fd6932012-07-11 10:26:13 -070035import android.os.PowerManager;
36import android.os.SystemClock;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070037import android.util.Log;
Victoria Lease4cd0a502012-11-02 16:24:08 -070038import android.util.Slog;
Nick Pelly4035f5a2012-08-17 14:43:49 -070039
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070040import com.android.server.FgThread;
Nick Pelly4035f5a2012-08-17 14:43:49 -070041import com.android.server.LocationManagerService;
Lifu Tang519f0d02018-04-12 16:39:39 -070042import com.android.server.PendingIntentUtils;
Nick Pellye0fd6932012-07-11 10:26:13 -070043
Soonil Nagarkar1c572552019-07-10 13:31:47 -070044import java.io.PrintWriter;
45import java.util.Iterator;
46import java.util.LinkedList;
47import java.util.List;
48
Nick Pellye0fd6932012-07-11 10:26:13 -070049public class GeofenceManager implements LocationListener, PendingIntent.OnFinished {
Nick Pelly6fa9ad42012-07-16 12:18:23 -070050 private static final String TAG = "GeofenceManager";
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070051 private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
Nick Pellye0fd6932012-07-11 10:26:13 -070052
Victoria Lease4cd0a502012-11-02 16:24:08 -070053 private static final int MSG_UPDATE_FENCES = 1;
54
Nick Pellye0fd6932012-07-11 10:26:13 -070055 /**
56 * Assume a maximum land speed, as a heuristic to throttle location updates.
57 * (Air travel should result in an airplane mode toggle which will
58 * force a new location update anyway).
59 */
Nick Pelly6fa9ad42012-07-16 12:18:23 -070060 private static final int MAX_SPEED_M_S = 100; // 360 km/hr (high speed train)
Nick Pellye0fd6932012-07-11 10:26:13 -070061
Victoria Lease4cd0a502012-11-02 16:24:08 -070062 /**
63 * Maximum age after which a location is no longer considered fresh enough to use.
64 */
65 private static final long MAX_AGE_NANOS = 5 * 60 * 1000000000L; // five minutes
66
Victoria Lease4cd0a502012-11-02 16:24:08 -070067
68 /**
69 * Least frequent update interval allowed.
70 */
71 private static final long MAX_INTERVAL_MS = 2 * 60 * 60 * 1000; // two hours
72
Nick Pelly6fa9ad42012-07-16 12:18:23 -070073 private final Context mContext;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070074 private final GeofenceHandler mHandler;
75
Nick Pelly6fa9ad42012-07-16 12:18:23 -070076 private final LocationManager mLocationManager;
Dianne Hackborn5e45ee62013-01-24 19:13:44 -080077 private final AppOpsManager mAppOps;
Nick Pelly6fa9ad42012-07-16 12:18:23 -070078 private final PowerManager.WakeLock mWakeLock;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -070079
Soonil Nagarkarb6375a42020-01-29 15:23:06 -080080 private final SettingsHelper mSettingsStore;
Nick Pellye0fd6932012-07-11 10:26:13 -070081
Soonil Nagarkar1c572552019-07-10 13:31:47 -070082 private final Object mLock = new Object();
Nick Pellye0fd6932012-07-11 10:26:13 -070083
Nick Pelly6fa9ad42012-07-16 12:18:23 -070084 // access to members below is synchronized on mLock
Victoria Lease4cd0a502012-11-02 16:24:08 -070085 /**
86 * A list containing all registered geofences.
87 */
Soonil Nagarkar1c572552019-07-10 13:31:47 -070088 private List<GeofenceState> mFences = new LinkedList<>();
Nick Pellye0fd6932012-07-11 10:26:13 -070089
Victoria Lease4cd0a502012-11-02 16:24:08 -070090 /**
91 * This is set true when we have an active request for {@link Location} updates via
92 * {@link LocationManager#requestLocationUpdates(LocationRequest, LocationListener,
93 * android.os.Looper).
94 */
95 private boolean mReceivingLocationUpdates;
96
97 /**
98 * The update interval component of the current active {@link Location} update request.
99 */
100 private long mLocationUpdateInterval;
101
102 /**
103 * The {@link Location} most recently received via {@link #onLocationChanged(Location)}.
104 */
105 private Location mLastLocationUpdate;
106
107 /**
108 * This is set true when a {@link Location} is received via
109 * {@link #onLocationChanged(Location)} or {@link #scheduleUpdateFencesLocked()}, and cleared
110 * when that Location has been processed via {@link #updateFences()}
111 */
112 private boolean mPendingUpdate;
113
Soonil Nagarkarb6375a42020-01-29 15:23:06 -0800114 public GeofenceManager(Context context, SettingsHelper settingsStore) {
Nick Pellye0fd6932012-07-11 10:26:13 -0700115 mContext = context;
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700116 mHandler = new GeofenceHandler(FgThread.getHandler().getLooper());
Lifu Tangc94ef4d2017-03-23 23:48:00 -0700117
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700118 mLocationManager = mContext.getSystemService(LocationManager.class);
119 mAppOps = mContext.getSystemService(AppOpsManager.class);
120
121 mWakeLock = mContext.getSystemService(PowerManager.class).newWakeLock(
122 PowerManager.PARTIAL_WAKE_LOCK, TAG);
123
124 mSettingsStore = settingsStore;
Nick Pellye0fd6932012-07-11 10:26:13 -0700125 }
126
Victoria Lease4cd0a502012-11-02 16:24:08 -0700127 public void addFence(LocationRequest request, Geofence geofence, PendingIntent intent,
Philip P. Moltmann6c7377c2019-09-27 17:06:25 -0700128 int allowedResolutionLevel, int uid, String packageName, @Nullable String featureId,
Philip P. Moltmannbc8b48a2019-09-27 17:06:25 -0700129 @NonNull String listenerIdentifier) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700130 if (D) {
131 Slog.d(TAG, "addFence: request=" + request + ", geofence=" + geofence
132 + ", intent=" + intent + ", uid=" + uid + ", packageName=" + packageName);
133 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700134
Victoria Lease4cd0a502012-11-02 16:24:08 -0700135 GeofenceState state = new GeofenceState(geofence,
Soonil Nagarkar95768ce2019-11-05 15:22:44 -0800136 request.getExpirationRealtimeMs(SystemClock.elapsedRealtime()),
137 allowedResolutionLevel, uid, packageName, featureId, listenerIdentifier, intent);
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700138 synchronized (mLock) {
139 // first make sure it doesn't already exist
140 for (int i = mFences.size() - 1; i >= 0; i--) {
141 GeofenceState w = mFences.get(i);
142 if (geofence.equals(w.mFence) && intent.equals(w.mIntent)) {
143 // already exists, remove the old one
144 mFences.remove(i);
145 break;
Nick Pellye0fd6932012-07-11 10:26:13 -0700146 }
147 }
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700148 mFences.add(state);
Victoria Lease4cd0a502012-11-02 16:24:08 -0700149 scheduleUpdateFencesLocked();
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700150 }
151 }
152
153 public void removeFence(Geofence fence, PendingIntent intent) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700154 if (D) {
155 Slog.d(TAG, "removeFence: fence=" + fence + ", intent=" + intent);
156 }
157
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700158 synchronized (mLock) {
159 Iterator<GeofenceState> iter = mFences.iterator();
160 while (iter.hasNext()) {
161 GeofenceState state = iter.next();
162 if (state.mIntent.equals(intent)) {
163
164 if (fence == null) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700165 // always remove
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700166 iter.remove();
167 } else {
168 // just remove matching fences
169 if (fence.equals(state.mFence)) {
170 iter.remove();
171 }
172 }
173 }
174 }
Victoria Lease4cd0a502012-11-02 16:24:08 -0700175 scheduleUpdateFencesLocked();
Nick Pellye0fd6932012-07-11 10:26:13 -0700176 }
177 }
178
179 public void removeFence(String packageName) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700180 if (D) {
181 Slog.d(TAG, "removeFence: packageName=" + packageName);
182 }
183
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700184 synchronized (mLock) {
185 Iterator<GeofenceState> iter = mFences.iterator();
Nick Pellye0fd6932012-07-11 10:26:13 -0700186 while (iter.hasNext()) {
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700187 GeofenceState state = iter.next();
188 if (state.mPackageName.equals(packageName)) {
Nick Pellye0fd6932012-07-11 10:26:13 -0700189 iter.remove();
190 }
191 }
Victoria Lease4cd0a502012-11-02 16:24:08 -0700192 scheduleUpdateFencesLocked();
Nick Pellye0fd6932012-07-11 10:26:13 -0700193 }
194 }
195
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700196 private void removeExpiredFencesLocked() {
197 long time = SystemClock.elapsedRealtime();
198 Iterator<GeofenceState> iter = mFences.iterator();
199 while (iter.hasNext()) {
200 GeofenceState state = iter.next();
201 if (state.mExpireAt < time) {
202 iter.remove();
Nick Pellye0fd6932012-07-11 10:26:13 -0700203 }
204 }
205 }
206
Victoria Lease4cd0a502012-11-02 16:24:08 -0700207 private void scheduleUpdateFencesLocked() {
208 if (!mPendingUpdate) {
209 mPendingUpdate = true;
210 mHandler.sendEmptyMessage(MSG_UPDATE_FENCES);
211 }
212 }
213
214 /**
215 * Returns the location received most recently from {@link #onLocationChanged(Location)},
216 * or consult {@link LocationManager#getLastLocation()} if none has arrived. Does not return
217 * either if the location would be too stale to be useful.
218 *
219 * @return a fresh, valid Location, or null if none is available
220 */
221 private Location getFreshLocationLocked() {
222 // Prefer mLastLocationUpdate to LocationManager.getLastLocation().
223 Location location = mReceivingLocationUpdates ? mLastLocationUpdate : null;
224 if (location == null && !mFences.isEmpty()) {
225 location = mLocationManager.getLastLocation();
226 }
227
228 // Early out for null location.
229 if (location == null) {
230 return null;
231 }
232
233 // Early out for stale location.
234 long now = SystemClock.elapsedRealtimeNanos();
235 if (now - location.getElapsedRealtimeNanos() > MAX_AGE_NANOS) {
236 return null;
237 }
238
239 // Made it this far? Return our fresh, valid location.
240 return location;
241 }
242
243 /**
244 * The geofence update loop. This function removes expired fences, then tests the most
245 * recently-received {@link Location} against each registered {@link GeofenceState}, sending
246 * {@link Intent}s for geofences that have been tripped. It also adjusts the active location
247 * update request with {@link LocationManager} as appropriate for any active geofences.
248 */
249 // Runs on the handler.
250 private void updateFences() {
Soonil Nagarkar1c572552019-07-10 13:31:47 -0700251 List<PendingIntent> enterIntents = new LinkedList<>();
252 List<PendingIntent> exitIntents = new LinkedList<>();
Nick Pellye0fd6932012-07-11 10:26:13 -0700253
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700254 synchronized (mLock) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700255 mPendingUpdate = false;
256
257 // Remove expired fences.
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700258 removeExpiredFencesLocked();
Nick Pellye0fd6932012-07-11 10:26:13 -0700259
Victoria Lease4cd0a502012-11-02 16:24:08 -0700260 // Get a location to work with, either received via onLocationChanged() or
261 // via LocationManager.getLastLocation().
262 Location location = getFreshLocationLocked();
263
264 // Update all fences.
265 // Keep track of the distance to the nearest fence.
266 double minFenceDistance = Double.MAX_VALUE;
267 boolean needUpdates = false;
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700268 for (GeofenceState state : mFences) {
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700269 if (mSettingsStore.isLocationPackageBlacklisted(ActivityManager.getCurrentUser(),
270 state.mPackageName)) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700271 if (D) {
272 Slog.d(TAG, "skipping geofence processing for blacklisted app: "
273 + state.mPackageName);
274 }
Nick Pelly4035f5a2012-08-17 14:43:49 -0700275 continue;
276 }
277
Dianne Hackborn5e45ee62013-01-24 19:13:44 -0800278 int op = LocationManagerService.resolutionLevelToOp(state.mAllowedResolutionLevel);
279 if (op >= 0) {
280 if (mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, state.mUid,
Philip P. Moltmann6c7377c2019-09-27 17:06:25 -0700281 state.mPackageName, state.mFeatureId, state.mListenerIdentifier)
Philip P. Moltmannbc8b48a2019-09-27 17:06:25 -0700282 != AppOpsManager.MODE_ALLOWED) {
Dianne Hackborn5e45ee62013-01-24 19:13:44 -0800283 if (D) {
284 Slog.d(TAG, "skipping geofence processing for no op app: "
285 + state.mPackageName);
286 }
287 continue;
288 }
289 }
290
Victoria Lease4cd0a502012-11-02 16:24:08 -0700291 needUpdates = true;
292 if (location != null) {
293 int event = state.processLocation(location);
294 if ((event & GeofenceState.FLAG_ENTER) != 0) {
295 enterIntents.add(state.mIntent);
296 }
297 if ((event & GeofenceState.FLAG_EXIT) != 0) {
298 exitIntents.add(state.mIntent);
299 }
300
301 // FIXME: Ideally this code should take into account the accuracy of the
302 // location fix that was used to calculate the distance in the first place.
303 double fenceDistance = state.getDistanceToBoundary(); // MAX_VALUE if unknown
304 if (fenceDistance < minFenceDistance) {
305 minFenceDistance = fenceDistance;
306 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700307 }
308 }
Victoria Lease4cd0a502012-11-02 16:24:08 -0700309
310 // Request or cancel location updates if needed.
311 if (needUpdates) {
312 // Request location updates.
313 // Compute a location update interval based on the distance to the nearest fence.
314 long intervalMs;
315 if (location != null && Double.compare(minFenceDistance, Double.MAX_VALUE) != 0) {
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700316 intervalMs = (long) Math.min(MAX_INTERVAL_MS, Math.max(
317 mSettingsStore.getBackgroundThrottleProximityAlertIntervalMs(),
Victoria Lease4cd0a502012-11-02 16:24:08 -0700318 minFenceDistance * 1000 / MAX_SPEED_M_S));
319 } else {
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700320 intervalMs = mSettingsStore.getBackgroundThrottleProximityAlertIntervalMs();
Victoria Lease4cd0a502012-11-02 16:24:08 -0700321 }
322 if (!mReceivingLocationUpdates || mLocationUpdateInterval != intervalMs) {
323 mReceivingLocationUpdates = true;
324 mLocationUpdateInterval = intervalMs;
325 mLastLocationUpdate = location;
326
327 LocationRequest request = new LocationRequest();
328 request.setInterval(intervalMs).setFastestInterval(0);
329 mLocationManager.requestLocationUpdates(request, this, mHandler.getLooper());
330 }
331 } else {
332 // Cancel location updates.
333 if (mReceivingLocationUpdates) {
334 mReceivingLocationUpdates = false;
335 mLocationUpdateInterval = 0;
336 mLastLocationUpdate = null;
337
338 mLocationManager.removeUpdates(this);
339 }
340 }
341
342 if (D) {
343 Slog.d(TAG, "updateFences: location=" + location
344 + ", mFences.size()=" + mFences.size()
345 + ", mReceivingLocationUpdates=" + mReceivingLocationUpdates
346 + ", mLocationUpdateInterval=" + mLocationUpdateInterval
347 + ", mLastLocationUpdate=" + mLastLocationUpdate);
348 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700349 }
350
351 // release lock before sending intents
352 for (PendingIntent intent : exitIntents) {
353 sendIntentExit(intent);
354 }
355 for (PendingIntent intent : enterIntents) {
356 sendIntentEnter(intent);
357 }
358 }
359
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700360 private void sendIntentEnter(PendingIntent pendingIntent) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700361 if (D) {
362 Slog.d(TAG, "sendIntentEnter: pendingIntent=" + pendingIntent);
363 }
364
Nick Pellye0fd6932012-07-11 10:26:13 -0700365 Intent intent = new Intent();
366 intent.putExtra(LocationManager.KEY_PROXIMITY_ENTERING, true);
367 sendIntent(pendingIntent, intent);
368 }
369
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700370 private void sendIntentExit(PendingIntent pendingIntent) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700371 if (D) {
372 Slog.d(TAG, "sendIntentExit: pendingIntent=" + pendingIntent);
373 }
374
Nick Pellye0fd6932012-07-11 10:26:13 -0700375 Intent intent = new Intent();
376 intent.putExtra(LocationManager.KEY_PROXIMITY_ENTERING, false);
377 sendIntent(pendingIntent, intent);
378 }
379
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700380 private void sendIntent(PendingIntent pendingIntent, Intent intent) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700381 mWakeLock.acquire();
Nick Pellye0fd6932012-07-11 10:26:13 -0700382 try {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700383 pendingIntent.send(mContext, 0, intent, this, null,
Lifu Tang519f0d02018-04-12 16:39:39 -0700384 android.Manifest.permission.ACCESS_FINE_LOCATION,
385 PendingIntentUtils.createDontSendToRestrictedAppsBundle(null));
Nick Pellye0fd6932012-07-11 10:26:13 -0700386 } catch (PendingIntent.CanceledException e) {
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700387 removeFence(null, pendingIntent);
Nick Pellye0fd6932012-07-11 10:26:13 -0700388 mWakeLock.release();
389 }
Victoria Lease4cd0a502012-11-02 16:24:08 -0700390 // ...otherwise, mWakeLock.release() gets called by onSendFinished()
Nick Pellye0fd6932012-07-11 10:26:13 -0700391 }
392
Victoria Lease4cd0a502012-11-02 16:24:08 -0700393 // Runs on the handler (which was passed into LocationManager.requestLocationUpdates())
Nick Pellye0fd6932012-07-11 10:26:13 -0700394 @Override
395 public void onLocationChanged(Location location) {
Victoria Lease4cd0a502012-11-02 16:24:08 -0700396 synchronized (mLock) {
397 if (mReceivingLocationUpdates) {
398 mLastLocationUpdate = location;
399 }
400
401 // Update the fences immediately before returning in
402 // case the caller is holding a wakelock.
403 if (mPendingUpdate) {
404 mHandler.removeMessages(MSG_UPDATE_FENCES);
405 } else {
406 mPendingUpdate = true;
407 }
408 }
409 updateFences();
Nick Pellye0fd6932012-07-11 10:26:13 -0700410 }
411
412 @Override
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700413 public void onStatusChanged(String provider, int status, Bundle extras) {
414 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700415
416 @Override
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700417 public void onProviderEnabled(String provider) {
418 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700419
420 @Override
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700421 public void onProviderDisabled(String provider) {
422 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700423
424 @Override
425 public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
426 String resultData, Bundle resultExtras) {
427 mWakeLock.release();
428 }
429
430 public void dump(PrintWriter pw) {
Nick Pelly6fa9ad42012-07-16 12:18:23 -0700431 for (GeofenceState state : mFences) {
Soonil Nagarkar1c572552019-07-10 13:31:47 -0700432 pw.println(state.mPackageName + " " + state.mFence);
Nick Pellye0fd6932012-07-11 10:26:13 -0700433 }
434 }
Victoria Lease4cd0a502012-11-02 16:24:08 -0700435
436 private final class GeofenceHandler extends Handler {
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700437 private GeofenceHandler(Looper looper) {
438 super(looper);
Victoria Lease4cd0a502012-11-02 16:24:08 -0700439 }
440
441 @Override
442 public void handleMessage(Message msg) {
Soonil Nagarkarb8466b72019-10-25 14:10:30 -0700443 if (msg.what == MSG_UPDATE_FENCES) {
444 updateFences();
Victoria Lease4cd0a502012-11-02 16:24:08 -0700445 }
446 }
447 }
Nick Pellye0fd6932012-07-11 10:26:13 -0700448}