blob: d3067ec3a5c8e93df75560eee96a30b60e01e25d [file] [log] [blame]
Christopher Tate487529a2009-04-29 14:03:25 -07001/*
2 * Copyright (C) 2009 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;
18
Christopher Tate181fafa2009-05-14 11:12:14 -070019import android.app.ActivityManagerNative;
20import android.app.IActivityManager;
21import android.app.IApplicationThread;
22import android.app.IBackupAgent;
Christopher Tate3799bc22009-05-06 16:13:56 -070023import android.content.BroadcastReceiver;
Christopher Tate487529a2009-04-29 14:03:25 -070024import android.content.Context;
25import android.content.Intent;
Christopher Tate3799bc22009-05-06 16:13:56 -070026import android.content.IntentFilter;
Christopher Tate181fafa2009-05-14 11:12:14 -070027import android.content.pm.ApplicationInfo;
Christopher Tatec7b31e32009-06-10 15:49:30 -070028import android.content.pm.IPackageDataObserver;
Christopher Tate7b881282009-06-07 13:52:37 -070029import android.content.pm.PackageInfo;
Christopher Tate487529a2009-04-29 14:03:25 -070030import android.content.pm.PackageManager;
Christopher Tate043dadc2009-06-02 16:11:00 -070031import android.content.pm.PackageManager.NameNotFoundException;
Christopher Tate3799bc22009-05-06 16:13:56 -070032import android.net.Uri;
Christopher Tate487529a2009-04-29 14:03:25 -070033import android.os.Binder;
Christopher Tate3799bc22009-05-06 16:13:56 -070034import android.os.Bundle;
Christopher Tate22b87872009-05-04 16:41:53 -070035import android.os.Environment;
Christopher Tate487529a2009-04-29 14:03:25 -070036import android.os.Handler;
37import android.os.IBinder;
38import android.os.Message;
Christopher Tate22b87872009-05-04 16:41:53 -070039import android.os.ParcelFileDescriptor;
Christopher Tate043dadc2009-06-02 16:11:00 -070040import android.os.Process;
Christopher Tate487529a2009-04-29 14:03:25 -070041import android.os.RemoteException;
42import android.util.Log;
43import android.util.SparseArray;
44
45import android.backup.IBackupManager;
Christopher Tate8c850b72009-06-07 19:33:20 -070046import android.backup.IRestoreSession;
Christopher Tate043dadc2009-06-02 16:11:00 -070047import android.backup.BackupManager;
Christopher Tate9b3905c2009-06-08 15:24:01 -070048import android.backup.RestoreSet;
Christopher Tate043dadc2009-06-02 16:11:00 -070049
Christopher Tate9bbc21a2009-06-10 20:23:25 -070050import com.android.internal.backup.LocalTransport;
Christopher Tate043dadc2009-06-02 16:11:00 -070051import com.android.internal.backup.GoogleTransport;
52import com.android.internal.backup.IBackupTransport;
Christopher Tate487529a2009-04-29 14:03:25 -070053
Christopher Tate22b87872009-05-04 16:41:53 -070054import java.io.File;
Joe Onoratob1a7ffe2009-05-06 18:06:21 -070055import java.io.FileDescriptor;
Christopher Tate22b87872009-05-04 16:41:53 -070056import java.io.FileNotFoundException;
Christopher Tatec7b31e32009-06-10 15:49:30 -070057import java.io.IOException;
Joe Onoratob1a7ffe2009-05-06 18:06:21 -070058import java.io.PrintWriter;
Christopher Tate487529a2009-04-29 14:03:25 -070059import java.lang.String;
Joe Onorato8ad02812009-05-13 01:41:44 -040060import java.util.ArrayList;
61import java.util.HashMap;
Christopher Tate487529a2009-04-29 14:03:25 -070062import java.util.HashSet;
Christopher Tate181fafa2009-05-14 11:12:14 -070063import java.util.Iterator;
Christopher Tate487529a2009-04-29 14:03:25 -070064import java.util.List;
65
66class BackupManagerService extends IBackupManager.Stub {
67 private static final String TAG = "BackupManagerService";
68 private static final boolean DEBUG = true;
69
Joe Onoratob1a7ffe2009-05-06 18:06:21 -070070 private static final long COLLECTION_INTERVAL = 1000;
71 //private static final long COLLECTION_INTERVAL = 3 * 60 * 1000;
Christopher Tate487529a2009-04-29 14:03:25 -070072
73 private static final int MSG_RUN_BACKUP = 1;
Christopher Tate043dadc2009-06-02 16:11:00 -070074 private static final int MSG_RUN_FULL_BACKUP = 2;
Christopher Tate9bbc21a2009-06-10 20:23:25 -070075 private static final int MSG_RUN_RESTORE = 3;
Christopher Tatec7b31e32009-06-10 15:49:30 -070076
77 // Timeout interval for deciding that a bind or clear-data has taken too long
78 static final long TIMEOUT_INTERVAL = 10 * 1000;
79
Christopher Tate487529a2009-04-29 14:03:25 -070080 private Context mContext;
81 private PackageManager mPackageManager;
Christopher Tate181fafa2009-05-14 11:12:14 -070082 private final IActivityManager mActivityManager;
Christopher Tate487529a2009-04-29 14:03:25 -070083 private final BackupHandler mBackupHandler = new BackupHandler();
84 // map UIDs to the set of backup client services within that UID's app set
Christopher Tate181fafa2009-05-14 11:12:14 -070085 private SparseArray<HashSet<ApplicationInfo>> mBackupParticipants
86 = new SparseArray<HashSet<ApplicationInfo>>();
Christopher Tate487529a2009-04-29 14:03:25 -070087 // set of backup services that have pending changes
Christopher Tate46758122009-05-06 11:22:00 -070088 private class BackupRequest {
Christopher Tate181fafa2009-05-14 11:12:14 -070089 public ApplicationInfo appInfo;
Christopher Tate46758122009-05-06 11:22:00 -070090 public boolean fullBackup;
91
Christopher Tate181fafa2009-05-14 11:12:14 -070092 BackupRequest(ApplicationInfo app, boolean isFull) {
93 appInfo = app;
Christopher Tate46758122009-05-06 11:22:00 -070094 fullBackup = isFull;
95 }
Christopher Tate181fafa2009-05-14 11:12:14 -070096
97 public String toString() {
98 return "BackupRequest{app=" + appInfo + " full=" + fullBackup + "}";
99 }
Christopher Tate46758122009-05-06 11:22:00 -0700100 }
Joe Onorato8ad02812009-05-13 01:41:44 -0400101 // Backups that we haven't started yet.
Christopher Tate181fafa2009-05-14 11:12:14 -0700102 private HashMap<ApplicationInfo,BackupRequest> mPendingBackups
103 = new HashMap<ApplicationInfo,BackupRequest>();
Joe Onorato8ad02812009-05-13 01:41:44 -0400104 // Backups that we have started. These are separate to prevent starvation
105 // if an app keeps re-enqueuing itself.
106 private ArrayList<BackupRequest> mBackupQueue;
Christopher Tate487529a2009-04-29 14:03:25 -0700107 private final Object mQueueLock = new Object();
108
Christopher Tate043dadc2009-06-02 16:11:00 -0700109 // The thread performing the sequence of queued backups binds to each app's agent
110 // in succession. Bind notifications are asynchronously delivered through the
111 // Activity Manager; use this lock object to signal when a requested binding has
112 // completed.
113 private final Object mAgentConnectLock = new Object();
114 private IBackupAgent mConnectedAgent;
115 private volatile boolean mConnecting;
116
Christopher Tatec7b31e32009-06-10 15:49:30 -0700117 // A similar synchronicity mechanism around clearing apps' data for restore
118 private final Object mClearDataLock = new Object();
119 private volatile boolean mClearingData;
120
Christopher Tate043dadc2009-06-02 16:11:00 -0700121 private int mTransportId;
122
Christopher Tate22b87872009-05-04 16:41:53 -0700123 private File mStateDir;
Christopher Tatef4172472009-05-05 15:50:03 -0700124 private File mDataDir;
Christopher Tate487529a2009-04-29 14:03:25 -0700125
Christopher Tate487529a2009-04-29 14:03:25 -0700126 public BackupManagerService(Context context) {
127 mContext = context;
128 mPackageManager = context.getPackageManager();
Christopher Tate181fafa2009-05-14 11:12:14 -0700129 mActivityManager = ActivityManagerNative.getDefault();
Christopher Tate487529a2009-04-29 14:03:25 -0700130
Christopher Tate22b87872009-05-04 16:41:53 -0700131 // Set up our bookkeeping
Christopher Tatef4172472009-05-05 15:50:03 -0700132 mStateDir = new File(Environment.getDataDirectory(), "backup");
Christopher Tate22b87872009-05-04 16:41:53 -0700133 mStateDir.mkdirs();
Christopher Tatef4172472009-05-05 15:50:03 -0700134 mDataDir = Environment.getDownloadCacheDirectory();
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700135
136 //!!! TODO: default to cloud transport, not local
137 mTransportId = BackupManager.TRANSPORT_LOCAL;
Christopher Tate22b87872009-05-04 16:41:53 -0700138
Christopher Tate3799bc22009-05-06 16:13:56 -0700139 // Build our mapping of uid to backup client services
140 synchronized (mBackupParticipants) {
141 addPackageParticipantsLocked(null);
Christopher Tate487529a2009-04-29 14:03:25 -0700142 }
143
Christopher Tate3799bc22009-05-06 16:13:56 -0700144 // Register for broadcasts about package install, etc., so we can
145 // update the provider list.
146 IntentFilter filter = new IntentFilter();
147 filter.addAction(Intent.ACTION_PACKAGE_ADDED);
148 filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
149 filter.addDataScheme("package");
150 mContext.registerReceiver(mBroadcastReceiver, filter);
151 }
152
153 // ----- Track installation/removal of packages -----
154 BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
155 public void onReceive(Context context, Intent intent) {
156 if (DEBUG) Log.d(TAG, "Received broadcast " + intent);
157
158 Uri uri = intent.getData();
159 if (uri == null) {
160 return;
Christopher Tate487529a2009-04-29 14:03:25 -0700161 }
Christopher Tate3799bc22009-05-06 16:13:56 -0700162 String pkgName = uri.getSchemeSpecificPart();
163 if (pkgName == null) {
164 return;
165 }
166
167 String action = intent.getAction();
168 if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
169 synchronized (mBackupParticipants) {
170 Bundle extras = intent.getExtras();
171 if (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false)) {
172 // The package was just upgraded
173 updatePackageParticipantsLocked(pkgName);
174 } else {
175 // The package was just added
176 addPackageParticipantsLocked(pkgName);
177 }
178 }
179 }
180 else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
181 Bundle extras = intent.getExtras();
182 if (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false)) {
183 // The package is being updated. We'll receive a PACKAGE_ADDED shortly.
184 } else {
185 synchronized (mBackupParticipants) {
186 removePackageParticipantsLocked(pkgName);
187 }
188 }
189 }
190 }
191 };
192
Joe Onorato8ad02812009-05-13 01:41:44 -0400193 // ----- Run the actual backup process asynchronously -----
194
Christopher Tate181fafa2009-05-14 11:12:14 -0700195 private class BackupHandler extends Handler {
Joe Onorato8ad02812009-05-13 01:41:44 -0400196 public void handleMessage(Message msg) {
197
198 switch (msg.what) {
199 case MSG_RUN_BACKUP:
200 // snapshot the pending-backup set and work on that
201 synchronized (mQueueLock) {
Joe Onoratod2110db2009-05-19 13:41:21 -0700202 if (mBackupQueue == null) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700203 mBackupQueue = new ArrayList<BackupRequest>();
Joe Onoratod2110db2009-05-19 13:41:21 -0700204 for (BackupRequest b: mPendingBackups.values()) {
205 mBackupQueue.add(b);
206 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700207 mPendingBackups = new HashMap<ApplicationInfo,BackupRequest>();
Joe Onorato8ad02812009-05-13 01:41:44 -0400208 }
Joe Onorato8ad02812009-05-13 01:41:44 -0400209 // !!! TODO: start a new backup-queue journal file too
210 // WARNING: If we crash after this line, anything in mPendingBackups will
211 // be lost. FIX THIS.
212 }
Christopher Tate043dadc2009-06-02 16:11:00 -0700213 (new PerformBackupThread(mTransportId, mBackupQueue)).run();
214 break;
215
216 case MSG_RUN_FULL_BACKUP:
Joe Onorato8ad02812009-05-13 01:41:44 -0400217 break;
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700218
219 case MSG_RUN_RESTORE:
220 {
221 int token = msg.arg1;
222 IBackupTransport transport = (IBackupTransport)msg.obj;
223 (new PerformRestoreThread(transport, token)).run();
224 break;
225 }
Joe Onorato8ad02812009-05-13 01:41:44 -0400226 }
227 }
Joe Onorato8ad02812009-05-13 01:41:44 -0400228 }
229
Christopher Tate181fafa2009-05-14 11:12:14 -0700230 // Add the backup agents in the given package to our set of known backup participants.
231 // If 'packageName' is null, adds all backup agents in the whole system.
Christopher Tate3799bc22009-05-06 16:13:56 -0700232 void addPackageParticipantsLocked(String packageName) {
Christopher Tate181fafa2009-05-14 11:12:14 -0700233 // Look for apps that define the android:backupAgent attribute
Christopher Tate043dadc2009-06-02 16:11:00 -0700234 if (DEBUG) Log.v(TAG, "addPackageParticipantsLocked: " + packageName);
Christopher Tate181fafa2009-05-14 11:12:14 -0700235 List<ApplicationInfo> targetApps = allAgentApps();
236 addPackageParticipantsLockedInner(packageName, targetApps);
Christopher Tate3799bc22009-05-06 16:13:56 -0700237 }
238
Christopher Tate181fafa2009-05-14 11:12:14 -0700239 private void addPackageParticipantsLockedInner(String packageName,
240 List<ApplicationInfo> targetApps) {
241 if (DEBUG) {
242 Log.v(TAG, "Adding " + targetApps.size() + " backup participants:");
243 for (ApplicationInfo a : targetApps) {
244 Log.v(TAG, " " + a + " agent=" + a.backupAgentName);
245 }
246 }
247
248 for (ApplicationInfo app : targetApps) {
249 if (packageName == null || app.packageName.equals(packageName)) {
250 int uid = app.uid;
251 HashSet<ApplicationInfo> set = mBackupParticipants.get(uid);
Christopher Tate3799bc22009-05-06 16:13:56 -0700252 if (set == null) {
Christopher Tate181fafa2009-05-14 11:12:14 -0700253 set = new HashSet<ApplicationInfo>();
Christopher Tate3799bc22009-05-06 16:13:56 -0700254 mBackupParticipants.put(uid, set);
255 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700256 set.add(app);
Christopher Tate3799bc22009-05-06 16:13:56 -0700257 }
Christopher Tate487529a2009-04-29 14:03:25 -0700258 }
259 }
260
Christopher Tate3799bc22009-05-06 16:13:56 -0700261 // Remove the given package's backup services from our known active set. If
262 // 'packageName' is null, *all* backup services will be removed.
263 void removePackageParticipantsLocked(String packageName) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700264 if (DEBUG) Log.v(TAG, "removePackageParticipantsLocked: " + packageName);
Christopher Tate181fafa2009-05-14 11:12:14 -0700265 List<ApplicationInfo> allApps = null;
266 if (packageName != null) {
267 allApps = new ArrayList<ApplicationInfo>();
268 try {
269 ApplicationInfo app = mPackageManager.getApplicationInfo(packageName, 0);
270 allApps.add(app);
271 } catch (Exception e) {
272 // just skip it
273 }
274 } else {
275 // all apps with agents
276 allApps = allAgentApps();
277 }
278 removePackageParticipantsLockedInner(packageName, allApps);
Christopher Tate3799bc22009-05-06 16:13:56 -0700279 }
280
Joe Onorato8ad02812009-05-13 01:41:44 -0400281 private void removePackageParticipantsLockedInner(String packageName,
Christopher Tate181fafa2009-05-14 11:12:14 -0700282 List<ApplicationInfo> agents) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700283 if (DEBUG) {
284 Log.v(TAG, "removePackageParticipantsLockedInner (" + packageName
285 + ") removing " + agents.size() + " entries");
286 for (ApplicationInfo a : agents) {
287 Log.v(TAG, " - " + a);
288 }
289 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700290 for (ApplicationInfo app : agents) {
291 if (packageName == null || app.packageName.equals(packageName)) {
292 int uid = app.uid;
293 HashSet<ApplicationInfo> set = mBackupParticipants.get(uid);
Christopher Tate3799bc22009-05-06 16:13:56 -0700294 if (set != null) {
Christopher Tatecd4ff2e2009-06-05 13:57:54 -0700295 // Find the existing entry with the same package name, and remove it.
296 // We can't just remove(app) because the instances are different.
297 for (ApplicationInfo entry: set) {
298 if (entry.packageName.equals(app.packageName)) {
299 set.remove(entry);
300 break;
301 }
302 }
Christopher Tate3799bc22009-05-06 16:13:56 -0700303 if (set.size() == 0) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700304 mBackupParticipants.delete(uid); }
Christopher Tate3799bc22009-05-06 16:13:56 -0700305 }
306 }
307 }
308 }
309
Christopher Tate181fafa2009-05-14 11:12:14 -0700310 // Returns the set of all applications that define an android:backupAgent attribute
311 private List<ApplicationInfo> allAgentApps() {
312 List<ApplicationInfo> allApps = mPackageManager.getInstalledApplications(0);
313 int N = allApps.size();
314 if (N > 0) {
315 for (int a = N-1; a >= 0; a--) {
316 ApplicationInfo app = allApps.get(a);
Christopher Tatedf01dea2009-06-09 20:45:02 -0700317 if (((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0)
318 || app.backupAgentName == null) {
Christopher Tate181fafa2009-05-14 11:12:14 -0700319 allApps.remove(a);
320 }
321 }
322 }
323 return allApps;
324 }
325
Christopher Tate3799bc22009-05-06 16:13:56 -0700326 // Reset the given package's known backup participants. Unlike add/remove, the update
327 // action cannot be passed a null package name.
328 void updatePackageParticipantsLocked(String packageName) {
329 if (packageName == null) {
330 Log.e(TAG, "updatePackageParticipants called with null package name");
331 return;
332 }
Christopher Tate043dadc2009-06-02 16:11:00 -0700333 if (DEBUG) Log.v(TAG, "updatePackageParticipantsLocked: " + packageName);
Christopher Tate3799bc22009-05-06 16:13:56 -0700334
335 // brute force but small code size
Christopher Tate181fafa2009-05-14 11:12:14 -0700336 List<ApplicationInfo> allApps = allAgentApps();
337 removePackageParticipantsLockedInner(packageName, allApps);
338 addPackageParticipantsLockedInner(packageName, allApps);
Christopher Tate3799bc22009-05-06 16:13:56 -0700339 }
340
Christopher Tate8c850b72009-06-07 19:33:20 -0700341 // Instantiate the given transport
342 private IBackupTransport createTransport(int transportID) {
343 IBackupTransport transport = null;
344 switch (transportID) {
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700345 case BackupManager.TRANSPORT_LOCAL:
346 if (DEBUG) Log.v(TAG, "Initializing local transport");
347 transport = new LocalTransport(mContext);
Christopher Tate8c850b72009-06-07 19:33:20 -0700348 break;
349
350 case BackupManager.TRANSPORT_GOOGLE:
351 if (DEBUG) Log.v(TAG, "Initializing Google transport");
352 //!!! TODO: stand up the google backup transport for real here
353 transport = new GoogleTransport();
354 break;
355
356 default:
357 Log.e(TAG, "creating unknown transport " + transportID);
358 }
359 return transport;
360 }
361
Christopher Tatedf01dea2009-06-09 20:45:02 -0700362 // fire off a backup agent, blocking until it attaches or times out
363 IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) {
364 IBackupAgent agent = null;
365 synchronized(mAgentConnectLock) {
366 mConnecting = true;
367 mConnectedAgent = null;
368 try {
369 if (mActivityManager.bindBackupAgent(app, mode)) {
370 Log.d(TAG, "awaiting agent for " + app);
371
372 // success; wait for the agent to arrive
Christopher Tatec7b31e32009-06-10 15:49:30 -0700373 // only wait 10 seconds for the clear data to happen
374 long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL;
375 while (mConnecting && mConnectedAgent == null
376 && (System.currentTimeMillis() < timeoutMark)) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700377 try {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700378 mAgentConnectLock.wait(5000);
Christopher Tatedf01dea2009-06-09 20:45:02 -0700379 } catch (InterruptedException e) {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700380 // just bail
Christopher Tatedf01dea2009-06-09 20:45:02 -0700381 return null;
382 }
383 }
384
385 // if we timed out with no connect, abort and move on
386 if (mConnecting == true) {
387 Log.w(TAG, "Timeout waiting for agent " + app);
388 return null;
389 }
390 agent = mConnectedAgent;
391 }
392 } catch (RemoteException e) {
393 // can't happen
394 }
395 }
396 return agent;
397 }
398
Christopher Tatec7b31e32009-06-10 15:49:30 -0700399 // clear an application's data, blocking until the operation completes or times out
400 void clearApplicationDataSynchronous(String packageName) {
401 ClearDataObserver observer = new ClearDataObserver();
402
403 synchronized(mClearDataLock) {
404 mClearingData = true;
405 mPackageManager.clearApplicationUserData(packageName, observer);
406
407 // only wait 10 seconds for the clear data to happen
408 long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL;
409 while (mClearingData && (System.currentTimeMillis() < timeoutMark)) {
410 try {
411 mClearDataLock.wait(5000);
412 } catch (InterruptedException e) {
413 // won't happen, but still.
414 mClearingData = false;
415 }
416 }
417 }
418 }
419
420 class ClearDataObserver extends IPackageDataObserver.Stub {
421 public void onRemoveCompleted(String packageName, boolean succeeded)
422 throws android.os.RemoteException {
423 synchronized(mClearDataLock) {
424 mClearingData = false;
425 notifyAll();
426 }
427 }
428 }
429
Christopher Tate043dadc2009-06-02 16:11:00 -0700430 // ----- Back up a set of applications via a worker thread -----
431
432 class PerformBackupThread extends Thread {
433 private static final String TAG = "PerformBackupThread";
434 int mTransport;
435 ArrayList<BackupRequest> mQueue;
436
437 public PerformBackupThread(int transportId, ArrayList<BackupRequest> queue) {
438 mTransport = transportId;
439 mQueue = queue;
440 }
441
442 @Override
443 public void run() {
Christopher Tate043dadc2009-06-02 16:11:00 -0700444 if (DEBUG) Log.v(TAG, "Beginning backup of " + mQueue.size() + " targets");
445
446 // stand up the current transport
Christopher Tate8c850b72009-06-07 19:33:20 -0700447 IBackupTransport transport = createTransport(mTransport);
448 if (transport == null) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700449 return;
450 }
451
Christopher Tatedf01dea2009-06-09 20:45:02 -0700452 // start up the transport
453 try {
454 transport.startSession();
455 } catch (Exception e) {
456 Log.e(TAG, "Error session transport");
457 e.printStackTrace();
458 return;
459 }
460
Christopher Tate043dadc2009-06-02 16:11:00 -0700461 // The transport is up and running; now run all the backups in our queue
462 doQueuedBackups(transport);
463
464 // Finally, tear down the transport
465 try {
466 transport.endSession();
467 } catch (Exception e) {
468 Log.e(TAG, "Error ending transport");
469 e.printStackTrace();
470 }
471 }
472
473 private void doQueuedBackups(IBackupTransport transport) {
474 for (BackupRequest request : mQueue) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700475 Log.d(TAG, "starting agent for backup of " + request);
Christopher Tate043dadc2009-06-02 16:11:00 -0700476
477 IBackupAgent agent = null;
478 int mode = (request.fullBackup)
479 ? IApplicationThread.BACKUP_MODE_FULL
480 : IApplicationThread.BACKUP_MODE_INCREMENTAL;
481 try {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700482 agent = bindToAgentSynchronous(request.appInfo, mode);
483 if (agent != null) {
484 processOneBackup(request, agent, transport);
Christopher Tate043dadc2009-06-02 16:11:00 -0700485 }
Christopher Tatedf01dea2009-06-09 20:45:02 -0700486
487 // unbind even on timeout, just in case
488 mActivityManager.unbindBackupAgent(request.appInfo);
Christopher Tate043dadc2009-06-02 16:11:00 -0700489 } catch (SecurityException ex) {
490 // Try for the next one.
Christopher Tatec7b31e32009-06-10 15:49:30 -0700491 Log.d(TAG, "error in bind/backup", ex);
Christopher Tate043dadc2009-06-02 16:11:00 -0700492 } catch (RemoteException e) {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700493 Log.v(TAG, "bind/backup threw");
494 e.printStackTrace();
Christopher Tate043dadc2009-06-02 16:11:00 -0700495 }
Christopher Tatedf01dea2009-06-09 20:45:02 -0700496
Christopher Tate043dadc2009-06-02 16:11:00 -0700497 }
498 }
Christopher Tatec7b31e32009-06-10 15:49:30 -0700499
500 void processOneBackup(BackupRequest request, IBackupAgent agent, IBackupTransport transport) {
501 final String packageName = request.appInfo.packageName;
502 Log.d(TAG, "processOneBackup doBackup() on " + packageName);
503
504 try {
505 // Look up the package info & signatures. This is first so that if it
506 // throws an exception, there's no file setup yet that would need to
507 // be unraveled.
508 PackageInfo packInfo = mPackageManager.getPackageInfo(packageName,
509 PackageManager.GET_SIGNATURES);
510
511 // !!! TODO: get the state file dir from the transport
512 File savedStateName = new File(mStateDir, packageName);
513 File backupDataName = new File(mDataDir, packageName + ".data");
514 File newStateName = new File(mStateDir, packageName + ".new");
515
516 // In a full backup, we pass a null ParcelFileDescriptor as
517 // the saved-state "file"
518 ParcelFileDescriptor savedState = (request.fullBackup) ? null
519 : ParcelFileDescriptor.open(savedStateName,
520 ParcelFileDescriptor.MODE_READ_ONLY |
521 ParcelFileDescriptor.MODE_CREATE);
522
523 backupDataName.delete();
524 ParcelFileDescriptor backupData =
525 ParcelFileDescriptor.open(backupDataName,
526 ParcelFileDescriptor.MODE_READ_WRITE |
527 ParcelFileDescriptor.MODE_CREATE);
528
529 newStateName.delete();
530 ParcelFileDescriptor newState =
531 ParcelFileDescriptor.open(newStateName,
532 ParcelFileDescriptor.MODE_READ_WRITE |
533 ParcelFileDescriptor.MODE_CREATE);
534
535 // Run the target's backup pass
536 boolean success = false;
537 try {
538 agent.doBackup(savedState, backupData, newState);
539 success = true;
540 } finally {
541 if (savedState != null) {
542 savedState.close();
543 }
544 backupData.close();
545 newState.close();
546 }
547
548 // Now propagate the newly-backed-up data to the transport
549 if (success) {
550 if (DEBUG) Log.v(TAG, "doBackup() success; calling transport");
551 backupData =
552 ParcelFileDescriptor.open(backupDataName, ParcelFileDescriptor.MODE_READ_ONLY);
553 int error = transport.performBackup(packInfo, backupData);
554
555 // !!! TODO: After successful transport, delete the now-stale data
556 // and juggle the files so that next time the new state is passed
557 //backupDataName.delete();
558 newStateName.renameTo(savedStateName);
559 }
560 } catch (NameNotFoundException e) {
561 Log.e(TAG, "Package not found on backup: " + packageName);
562 } catch (FileNotFoundException fnf) {
563 Log.w(TAG, "File not found on backup: ");
564 fnf.printStackTrace();
565 } catch (RemoteException e) {
566 Log.d(TAG, "Remote target " + request.appInfo.packageName + " threw during backup:");
567 e.printStackTrace();
568 } catch (Exception e) {
569 Log.w(TAG, "Final exception guard in backup: ");
570 e.printStackTrace();
571 }
572 }
Christopher Tate043dadc2009-06-02 16:11:00 -0700573 }
574
Christopher Tatedf01dea2009-06-09 20:45:02 -0700575
576 // ----- Restore handling -----
577
578 // Is the given package restorable on this device? Returns the on-device app's
579 // ApplicationInfo struct if it is; null if not.
580 //
581 // !!! TODO: also consider signatures
Christopher Tatec7b31e32009-06-10 15:49:30 -0700582 PackageInfo isRestorable(PackageInfo packageInfo) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700583 if (packageInfo.packageName != null) {
584 try {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700585 PackageInfo app = mPackageManager.getPackageInfo(packageInfo.packageName,
Christopher Tatedf01dea2009-06-09 20:45:02 -0700586 PackageManager.GET_SIGNATURES);
Christopher Tatec7b31e32009-06-10 15:49:30 -0700587 if ((app.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700588 return app;
589 }
590 } catch (Exception e) {
591 // doesn't exist on this device, or other error -- just ignore it.
592 }
593 }
594 return null;
595 }
596
597 class PerformRestoreThread extends Thread {
598 private IBackupTransport mTransport;
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700599 private int mToken;
Christopher Tatec7b31e32009-06-10 15:49:30 -0700600 private RestoreSet mImage;
Christopher Tatedf01dea2009-06-09 20:45:02 -0700601
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700602 PerformRestoreThread(IBackupTransport transport, int restoreSetToken) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700603 mTransport = transport;
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700604 mToken = restoreSetToken;
Christopher Tatedf01dea2009-06-09 20:45:02 -0700605 }
606
607 @Override
608 public void run() {
609 /**
610 * Restore sequence:
611 *
612 * 1. start up the transport session
613 * 2. get the restore set description for our identity
614 * 3. for each app in the restore set:
615 * 3.a. if it's restorable on this device, add it to the restore queue
616 * 4. for each app in the restore queue:
Christopher Tatec7b31e32009-06-10 15:49:30 -0700617 * 4.a. clear the app data
Christopher Tatedf01dea2009-06-09 20:45:02 -0700618 * 4.b. get the restore data for the app from the transport
619 * 4.c. launch the backup agent for the app
620 * 4.d. agent.doRestore() with the data from the server
621 * 4.e. unbind the agent [and kill the app?]
622 * 5. shut down the transport
623 */
624
625 int err = -1;
626 try {
627 err = mTransport.startSession();
628 } catch (Exception e) {
629 Log.e(TAG, "Error starting transport for restore");
630 e.printStackTrace();
631 }
632
633 if (err == 0) {
634 // build the set of apps to restore
635 try {
636 RestoreSet[] images = mTransport.getAvailableRestoreSets();
637 if (images.length > 0) {
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700638 // !!! TODO: pick out the set for this token
Christopher Tatec7b31e32009-06-10 15:49:30 -0700639 mImage = images[0];
Christopher Tatedf01dea2009-06-09 20:45:02 -0700640
641 // build the set of apps we will attempt to restore
Christopher Tatec7b31e32009-06-10 15:49:30 -0700642 PackageInfo[] packages = mTransport.getAppSet(mImage.token);
643 HashSet<PackageInfo> appsToRestore = new HashSet<PackageInfo>();
Christopher Tatedf01dea2009-06-09 20:45:02 -0700644 for (PackageInfo pkg: packages) {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700645 // get the real PackageManager idea of the package
646 PackageInfo app = isRestorable(pkg);
Christopher Tatedf01dea2009-06-09 20:45:02 -0700647 if (app != null) {
648 appsToRestore.add(app);
649 }
650 }
651
652 // now run the restore queue
653 doQueuedRestores(appsToRestore);
654 }
655 } catch (RemoteException e) {
656 // can't happen; transports run locally
657 }
658
659 // done; shut down the transport
660 try {
661 mTransport.endSession();
662 } catch (Exception e) {
663 Log.e(TAG, "Error ending transport for restore");
664 e.printStackTrace();
665 }
666 }
667
668 // even if the initial session startup failed, report that we're done here
669 }
670
671 // restore each app in the queue
Christopher Tatec7b31e32009-06-10 15:49:30 -0700672 void doQueuedRestores(HashSet<PackageInfo> appsToRestore) {
673 for (PackageInfo app : appsToRestore) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700674 Log.d(TAG, "starting agent for restore of " + app);
675
Christopher Tatedf01dea2009-06-09 20:45:02 -0700676 try {
Christopher Tatec7b31e32009-06-10 15:49:30 -0700677 // Remove the app's data first
678 clearApplicationDataSynchronous(app.packageName);
679
680 // Now perform the restore into the clean app
681 IBackupAgent agent = bindToAgentSynchronous(app.applicationInfo,
682 IApplicationThread.BACKUP_MODE_RESTORE);
Christopher Tatedf01dea2009-06-09 20:45:02 -0700683 if (agent != null) {
684 processOneRestore(app, agent);
685 }
686
687 // unbind even on timeout, just in case
Christopher Tatec7b31e32009-06-10 15:49:30 -0700688 mActivityManager.unbindBackupAgent(app.applicationInfo);
Christopher Tatedf01dea2009-06-09 20:45:02 -0700689 } catch (SecurityException ex) {
690 // Try for the next one.
691 Log.d(TAG, "error in bind", ex);
692 } catch (RemoteException e) {
693 // can't happen
694 }
695
696 }
697 }
698
Christopher Tatec7b31e32009-06-10 15:49:30 -0700699 // Do the guts of a restore of one application, derived from the 'mImage'
700 // restore set via the 'mTransport' transport.
701 void processOneRestore(PackageInfo app, IBackupAgent agent) {
Christopher Tatedf01dea2009-06-09 20:45:02 -0700702 // !!! TODO: actually run the restore through mTransport
Christopher Tatec7b31e32009-06-10 15:49:30 -0700703 final String packageName = app.packageName;
704
705 // !!! TODO: get the dirs from the transport
706 File backupDataName = new File(mDataDir, packageName + ".restore");
707 backupDataName.delete();
708 try {
709 ParcelFileDescriptor backupData =
710 ParcelFileDescriptor.open(backupDataName,
711 ParcelFileDescriptor.MODE_READ_WRITE |
712 ParcelFileDescriptor.MODE_CREATE);
713
714 // Run the transport's restore pass
715 // Run the target's backup pass
716 int err = -1;
717 try {
718 err = mTransport.getRestoreData(mImage.token, app, backupData);
719 } catch (RemoteException e) {
720 // can't happen
721 } finally {
722 backupData.close();
723 }
724
725 // Okay, we have the data. Now have the agent do the restore.
726 File newStateName = new File(mStateDir, packageName + ".new");
727 ParcelFileDescriptor newState =
728 ParcelFileDescriptor.open(newStateName,
729 ParcelFileDescriptor.MODE_READ_WRITE |
730 ParcelFileDescriptor.MODE_CREATE);
731
732 backupData = ParcelFileDescriptor.open(backupDataName,
733 ParcelFileDescriptor.MODE_READ_ONLY);
734
735 boolean success = false;
736 try {
737 agent.doRestore(backupData, newState);
738 success = true;
739 } catch (Exception e) {
740 Log.e(TAG, "Restore failed for " + packageName);
741 e.printStackTrace();
742 } finally {
743 newState.close();
744 backupData.close();
745 }
746
747 // if everything went okay, remember the recorded state now
748 if (success) {
749 File savedStateName = new File(mStateDir, packageName);
750 newStateName.renameTo(savedStateName);
751 }
752 } catch (FileNotFoundException fnfe) {
753 Log.v(TAG, "Couldn't open file for restore: " + fnfe);
754 } catch (IOException ioe) {
755 Log.e(TAG, "Unable to process restore file: " + ioe);
756 } catch (Exception e) {
757 Log.e(TAG, "Final exception guard in restore:");
758 e.printStackTrace();
759 }
Christopher Tatedf01dea2009-06-09 20:45:02 -0700760 }
761 }
762
763
Christopher Tate487529a2009-04-29 14:03:25 -0700764 // ----- IBackupManager binder interface -----
Christopher Tatedf01dea2009-06-09 20:45:02 -0700765
Christopher Tatea8bf8152009-04-30 11:36:21 -0700766 public void dataChanged(String packageName) throws RemoteException {
Christopher Tate487529a2009-04-29 14:03:25 -0700767 // Record that we need a backup pass for the caller. Since multiple callers
768 // may share a uid, we need to note all candidates within that uid and schedule
769 // a backup pass for each of them.
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700770
771 Log.d(TAG, "dataChanged packageName=" + packageName);
Christopher Tate487529a2009-04-29 14:03:25 -0700772
Christopher Tate181fafa2009-05-14 11:12:14 -0700773 HashSet<ApplicationInfo> targets = mBackupParticipants.get(Binder.getCallingUid());
Christopher Tate487529a2009-04-29 14:03:25 -0700774 if (targets != null) {
775 synchronized (mQueueLock) {
776 // Note that this client has made data changes that need to be backed up
Christopher Tate181fafa2009-05-14 11:12:14 -0700777 for (ApplicationInfo app : targets) {
Christopher Tatea8bf8152009-04-30 11:36:21 -0700778 // validate the caller-supplied package name against the known set of
779 // packages associated with this uid
Christopher Tate181fafa2009-05-14 11:12:14 -0700780 if (app.packageName.equals(packageName)) {
Joe Onorato8ad02812009-05-13 01:41:44 -0400781 // Add the caller to the set of pending backups. If there is
782 // one already there, then overwrite it, but no harm done.
Christopher Tate181fafa2009-05-14 11:12:14 -0700783 BackupRequest req = new BackupRequest(app, false);
784 mPendingBackups.put(app, req);
Joe Onorato8ad02812009-05-13 01:41:44 -0400785 // !!! TODO: write to the pending-backup journal file in case of crash
Christopher Tate487529a2009-04-29 14:03:25 -0700786 }
787 }
788
Christopher Tate181fafa2009-05-14 11:12:14 -0700789 if (DEBUG) {
790 int numKeys = mPendingBackups.size();
791 Log.d(TAG, "Scheduling backup for " + numKeys + " participants:");
792 for (BackupRequest b : mPendingBackups.values()) {
793 Log.d(TAG, " + " + b + " agent=" + b.appInfo.backupAgentName);
794 }
795 }
Christopher Tate487529a2009-04-29 14:03:25 -0700796 // Schedule a backup pass in a few minutes. As backup-eligible data
797 // keeps changing, continue to defer the backup pass until things
798 // settle down, to avoid extra overhead.
Christopher Tate043dadc2009-06-02 16:11:00 -0700799 mBackupHandler.removeMessages(MSG_RUN_BACKUP);
Christopher Tate487529a2009-04-29 14:03:25 -0700800 mBackupHandler.sendEmptyMessageDelayed(MSG_RUN_BACKUP, COLLECTION_INTERVAL);
801 }
Christopher Tatedf01dea2009-06-09 20:45:02 -0700802 } else {
803 Log.w(TAG, "dataChanged but no participant pkg " + packageName);
Christopher Tate487529a2009-04-29 14:03:25 -0700804 }
805 }
Christopher Tate46758122009-05-06 11:22:00 -0700806
Christopher Tate043dadc2009-06-02 16:11:00 -0700807 // Schedule a backup pass for a given package. This method will schedule a
808 // full backup even for apps that do not declare an android:backupAgent, so
809 // use with care.
Christopher Tate46758122009-05-06 11:22:00 -0700810 public void scheduleFullBackup(String packageName) throws RemoteException {
Christopher Tate043dadc2009-06-02 16:11:00 -0700811 mContext.enforceCallingPermission("android.permission.BACKUP", "scheduleFullBackup");
812
813 if (DEBUG) Log.v(TAG, "Scheduling immediate full backup for " + packageName);
Christopher Tate46758122009-05-06 11:22:00 -0700814 synchronized (mQueueLock) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700815 try {
816 ApplicationInfo app = mPackageManager.getApplicationInfo(packageName, 0);
817 mPendingBackups.put(app, new BackupRequest(app, true));
818 mBackupHandler.sendEmptyMessage(MSG_RUN_FULL_BACKUP);
819 } catch (NameNotFoundException e) {
820 Log.w(TAG, "Could not find app for " + packageName + " to schedule full backup");
Christopher Tate46758122009-05-06 11:22:00 -0700821 }
822 }
823 }
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700824
Christopher Tate043dadc2009-06-02 16:11:00 -0700825 // Select which transport to use for the next backup operation
826 public int selectBackupTransport(int transportId) {
827 mContext.enforceCallingPermission("android.permission.BACKUP", "selectBackupTransport");
828
829 int prevTransport = mTransportId;
830 mTransportId = transportId;
831 return prevTransport;
832 }
833
834 // Callback: a requested backup agent has been instantiated. This should only
835 // be called from the Activity Manager.
Christopher Tate181fafa2009-05-14 11:12:14 -0700836 public void agentConnected(String packageName, IBinder agentBinder) {
Christopher Tate043dadc2009-06-02 16:11:00 -0700837 synchronized(mAgentConnectLock) {
838 if (Binder.getCallingUid() == Process.SYSTEM_UID) {
839 Log.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder);
840 IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder);
841 mConnectedAgent = agent;
842 mConnecting = false;
843 } else {
844 Log.w(TAG, "Non-system process uid=" + Binder.getCallingUid()
845 + " claiming agent connected");
846 }
847 mAgentConnectLock.notifyAll();
848 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700849 }
850
851 // Callback: a backup agent has failed to come up, or has unexpectedly quit.
852 // If the agent failed to come up in the first place, the agentBinder argument
Christopher Tate043dadc2009-06-02 16:11:00 -0700853 // will be null. This should only be called from the Activity Manager.
Christopher Tate181fafa2009-05-14 11:12:14 -0700854 public void agentDisconnected(String packageName) {
855 // TODO: handle backup being interrupted
Christopher Tate043dadc2009-06-02 16:11:00 -0700856 synchronized(mAgentConnectLock) {
857 if (Binder.getCallingUid() == Process.SYSTEM_UID) {
858 mConnectedAgent = null;
859 mConnecting = false;
860 } else {
861 Log.w(TAG, "Non-system process uid=" + Binder.getCallingUid()
862 + " claiming agent disconnected");
863 }
864 mAgentConnectLock.notifyAll();
865 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700866 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700867
Christopher Tate8c850b72009-06-07 19:33:20 -0700868 // Hand off a restore session
869 public IRestoreSession beginRestoreSession(int transportID) {
870 mContext.enforceCallingPermission("android.permission.BACKUP", "beginRestoreSession");
871 return null;
872 }
Christopher Tate043dadc2009-06-02 16:11:00 -0700873
Christopher Tate9b3905c2009-06-08 15:24:01 -0700874 // ----- Restore session -----
875
876 class RestoreSession extends IRestoreSession.Stub {
877 private IBackupTransport mRestoreTransport = null;
878 RestoreSet[] mRestoreSets = null;
879
880 RestoreSession(int transportID) {
881 mRestoreTransport = createTransport(transportID);
882 }
883
884 // --- Binder interface ---
885 public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException {
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700886 mContext.enforceCallingPermission("android.permission.BACKUP",
887 "getAvailableRestoreSets");
888
Christopher Tate9b3905c2009-06-08 15:24:01 -0700889 synchronized(this) {
890 if (mRestoreSets == null) {
891 mRestoreSets = mRestoreTransport.getAvailableRestoreSets();
892 }
893 return mRestoreSets;
894 }
895 }
896
897 public int performRestore(int token) throws android.os.RemoteException {
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700898 mContext.enforceCallingPermission("android.permission.BACKUP", "performRestore");
899
900 if (mRestoreSets != null) {
901 for (int i = 0; i < mRestoreSets.length; i++) {
902 if (token == mRestoreSets[i].token) {
903 Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE,
904 mRestoreTransport);
905 msg.arg1 = token;
906 mBackupHandler.sendMessage(msg);
907 return 0;
908 }
909 }
910 }
Christopher Tate9b3905c2009-06-08 15:24:01 -0700911 return -1;
912 }
913
914 public void endRestoreSession() throws android.os.RemoteException {
Christopher Tate9bbc21a2009-06-10 20:23:25 -0700915 mContext.enforceCallingPermission("android.permission.BACKUP",
916 "endRestoreSession");
917
Christopher Tate9b3905c2009-06-08 15:24:01 -0700918 mRestoreTransport.endSession();
919 mRestoreTransport = null;
920 }
921 }
922
Christopher Tate043dadc2009-06-02 16:11:00 -0700923
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700924 @Override
925 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
926 synchronized (mQueueLock) {
927 int N = mBackupParticipants.size();
928 pw.println("Participants:");
929 for (int i=0; i<N; i++) {
930 int uid = mBackupParticipants.keyAt(i);
931 pw.print(" uid: ");
932 pw.println(uid);
Christopher Tate181fafa2009-05-14 11:12:14 -0700933 HashSet<ApplicationInfo> participants = mBackupParticipants.valueAt(i);
934 for (ApplicationInfo app: participants) {
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700935 pw.print(" ");
Christopher Tate181fafa2009-05-14 11:12:14 -0700936 pw.println(app.toString());
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700937 }
938 }
Christopher Tate181fafa2009-05-14 11:12:14 -0700939 pw.println("Pending:");
940 Iterator<BackupRequest> br = mPendingBackups.values().iterator();
941 while (br.hasNext()) {
942 pw.print(" ");
943 pw.println(br);
944 }
Joe Onoratob1a7ffe2009-05-06 18:06:21 -0700945 }
946 }
Christopher Tate487529a2009-04-29 14:03:25 -0700947}