LocalTransport.java revision 25a747f5c47f25c1a18961b03507f309b84924fe
19bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tatepackage com.android.internal.backup;
29bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
32fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tateimport android.backup.BackupDataInput;
48e55eac96d768a4de68a091f57487deadf6d0a87Christopher Tateimport android.backup.BackupDataOutput;
59bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.backup.RestoreSet;
69bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.content.Context;
79bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.content.pm.PackageInfo;
89bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.content.pm.PackageManager;
99bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.content.pm.PackageManager.NameNotFoundException;
109bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.os.Environment;
119bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.os.ParcelFileDescriptor;
129bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.os.RemoteException;
139bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport android.util.Log;
149bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
15e9190a2750e1fb67e300d2c128227cc9b7339efeChristopher Tateimport org.bouncycastle.util.encoders.Base64;
16e9190a2750e1fb67e300d2c128227cc9b7339efeChristopher Tate
179bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.io.File;
189bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.io.FileFilter;
199bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.io.FileInputStream;
209bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.io.FileOutputStream;
219bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.io.IOException;
229bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tateimport java.util.ArrayList;
239bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
249bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate/**
259bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate * Backup transport for stashing stuff into a known location on disk, and
269bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate * later restoring from there.  For testing only.
279bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate */
289bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
299bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tatepublic class LocalTransport extends IBackupTransport.Stub {
309bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    private static final String TAG = "LocalTransport";
312fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate    private static final boolean DEBUG = true;
329bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
335cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate    private static final String TRANSPORT_DIR_NAME
345cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate            = "com.android.internal.backup.LocalTransport";
355cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate
369bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    private Context mContext;
379bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    private PackageManager mPackageManager;
389bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup");
39efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    private PackageInfo[] mRestorePackages = null;
40efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    private int mRestorePackage = -1;  // Index into mRestorePackages
419bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
429bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
439bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    public LocalTransport(Context context) {
442fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        if (DEBUG) Log.v(TAG, "Transport constructed");
459bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        mContext = context;
469bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        mPackageManager = context.getPackageManager();
479bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
489bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
495cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate
505cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate    public String transportDirName() throws RemoteException {
515cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate        return TRANSPORT_DIR_NAME;
525cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate    }
535cb400bd72726c22f641f334951b35ce2ddcfeefChristopher Tate
549bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    public long requestBackupTime() throws RemoteException {
559bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        // any time is a good time for local backup
569bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        return 0;
579bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
589bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
5925a747f5c47f25c1a18961b03507f309b84924feChristopher Tate    public boolean performBackup(PackageInfo packageInfo, ParcelFileDescriptor data,
6025a747f5c47f25c1a18961b03507f309b84924feChristopher Tate            boolean wipeAllFirst) throws RemoteException {
612fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName);
629bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
632fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        File packageDir = new File(mDataDir, packageInfo.packageName);
642fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        packageDir.mkdirs();
6525a747f5c47f25c1a18961b03507f309b84924feChristopher Tate        if (wipeAllFirst) {
6625a747f5c47f25c1a18961b03507f309b84924feChristopher Tate            if (DEBUG) Log.v(TAG, "wiping all data first");
6725a747f5c47f25c1a18961b03507f309b84924feChristopher Tate            deleteContents(mDataDir);
6825a747f5c47f25c1a18961b03507f309b84924feChristopher Tate        }
699bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
702fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // Each 'record' in the restore set is kept in its own file, named by
712fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // the record key.  Wind through the data file, extracting individual
722fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // record operations and building a set of all the updates to apply
732fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // in this update.
742fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
752fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        try {
762fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            int bufSize = 512;
772fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            byte[] buf = new byte[bufSize];
782fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            while (changeSet.readNextHeader()) {
792fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate                String key = changeSet.getKey();
805d605dc56b036232e885f6ec36b888b729673060Joe Onorato                String base64Key = new String(Base64.encode(key.getBytes()));
815d605dc56b036232e885f6ec36b888b729673060Joe Onorato                File entityFile = new File(packageDir, base64Key);
825d605dc56b036232e885f6ec36b888b729673060Joe Onorato
832fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate                int dataSize = changeSet.getDataSize();
84e9190a2750e1fb67e300d2c128227cc9b7339efeChristopher Tate
85e9190a2750e1fb67e300d2c128227cc9b7339efeChristopher Tate                if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize
86e9190a2750e1fb67e300d2c128227cc9b7339efeChristopher Tate                        + " key64=" + base64Key);
872fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate
885d605dc56b036232e885f6ec36b888b729673060Joe Onorato                if (dataSize >= 0) {
895d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    FileOutputStream entity = new FileOutputStream(entityFile);
905d605dc56b036232e885f6ec36b888b729673060Joe Onorato
915d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    if (dataSize > bufSize) {
925d605dc56b036232e885f6ec36b888b729673060Joe Onorato                        bufSize = dataSize;
935d605dc56b036232e885f6ec36b888b729673060Joe Onorato                        buf = new byte[bufSize];
945d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    }
955d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    changeSet.readEntityData(buf, 0, dataSize);
965d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    if (DEBUG) Log.v(TAG, "  data size " + dataSize);
975d605dc56b036232e885f6ec36b888b729673060Joe Onorato
985d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    try {
995d605dc56b036232e885f6ec36b888b729673060Joe Onorato                        entity.write(buf, 0, dataSize);
1005d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    } catch (IOException e) {
101efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                        Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath());
102efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                        return false;
1035d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    } finally {
1045d605dc56b036232e885f6ec36b888b729673060Joe Onorato                        entity.close();
1055d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    }
1065d605dc56b036232e885f6ec36b888b729673060Joe Onorato                } else {
1075d605dc56b036232e885f6ec36b888b729673060Joe Onorato                    entityFile.delete();
1082fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate                }
1092fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            }
110efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            return true;
1112fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        } catch (IOException e) {
1122fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            // oops, something went wrong.  abort the operation and return error.
113efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            Log.v(TAG, "Exception reading backup input:", e);
114efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            return false;
1152fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        }
116efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    }
117ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate
11825a747f5c47f25c1a18961b03507f309b84924feChristopher Tate    // Deletes the contents but not the given directory
11925a747f5c47f25c1a18961b03507f309b84924feChristopher Tate    private void deleteContents(File dirname) {
12025a747f5c47f25c1a18961b03507f309b84924feChristopher Tate        File[] contents = dirname.listFiles();
12125a747f5c47f25c1a18961b03507f309b84924feChristopher Tate        if (contents != null) {
12225a747f5c47f25c1a18961b03507f309b84924feChristopher Tate            for (File f : contents) {
12325a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                if (f.isDirectory()) {
12425a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                    // delete the directory's contents then fall through
12525a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                    // and delete the directory itself.
12625a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                    deleteContents(f);
12725a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                }
12825a747f5c47f25c1a18961b03507f309b84924feChristopher Tate                f.delete();
12925a747f5c47f25c1a18961b03507f309b84924feChristopher Tate            }
13025a747f5c47f25c1a18961b03507f309b84924feChristopher Tate        }
13125a747f5c47f25c1a18961b03507f309b84924feChristopher Tate    }
13225a747f5c47f25c1a18961b03507f309b84924feChristopher Tate
133ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate    public boolean clearBackupData(PackageInfo packageInfo) {
134ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName);
135ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate
136ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        File packageDir = new File(mDataDir, packageInfo.packageName);
137ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        for (File f : packageDir.listFiles()) {
138ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate            f.delete();
139ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        }
140ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        packageDir.delete();
141ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate        return true;
142ee0e78af5af3bf23dd928fe5e0ebeb39157eaf66Christopher Tate    }
1439bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
144efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    public boolean finishBackup() throws RemoteException {
145efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (DEBUG) Log.v(TAG, "finishBackup()");
146efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        return true;
1479bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
1489bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
1499bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    // Restore handling
1509bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException {
1519bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        // one hardcoded restore set
152f68eb500f99361541049e09eb7f9ddd6f4ef4efaChristopher Tate        RestoreSet set = new RestoreSet("Local disk image", "flash", 0);
153f68eb500f99361541049e09eb7f9ddd6f4ef4efaChristopher Tate        RestoreSet[] array = { set };
154f68eb500f99361541049e09eb7f9ddd6f4ef4efaChristopher Tate        return array;
1559bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
1569bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
157efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    public boolean startRestore(long token, PackageInfo[] packages) {
158efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (DEBUG) Log.v(TAG, "start restore " + token);
159efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        mRestorePackages = packages;
160efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        mRestorePackage = -1;
161efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        return true;
162efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    }
1639bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
164efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    public String nextRestorePackage() {
165efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
166efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        while (++mRestorePackage < mRestorePackages.length) {
167efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            String name = mRestorePackages[mRestorePackage].packageName;
168efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            if (new File(mDataDir, name).isDirectory()) {
169efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                if (DEBUG) Log.v(TAG, "  nextRestorePackage() = " + name);
170efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                return name;
1712fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            }
1729bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate        }
1739bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
174efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (DEBUG) Log.v(TAG, "  no more packages to restore");
175efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        return "";
1769bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
1779bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
178efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    public boolean getRestoreData(ParcelFileDescriptor outFd) {
179efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
180efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
181efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName);
1829bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate
1832fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // The restore set is the concatenation of the individual record blobs,
1842fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        // each of which is a file in the package's directory
1852fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        File[] blobs = packageDir.listFiles();
186efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (blobs == null) {
187efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            Log.e(TAG, "Error listing directory: " + packageDir);
188efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            return false;  // nextRestorePackage() ensures the dir exists, so this is an error
189efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        }
190efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor
191efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        // We expect at least some data if the directory exists in the first place
192efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.length + " key files");
193efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
194efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        try {
195efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            for (File f : blobs) {
196efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                FileInputStream in = new FileInputStream(f);
197efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                try {
198efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    int size = (int) f.length();
199efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    byte[] buf = new byte[size];
200efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    in.read(buf);
201efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    String key = new String(Base64.decode(f.getName()));
202efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    if (DEBUG) Log.v(TAG, "    ... key=" + key + " size=" + size);
203efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    out.writeEntityHeader(key, size);
204efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    out.writeEntityData(buf, size);
205efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                } finally {
206efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor                    in.close();
2078e55eac96d768a4de68a091f57487deadf6d0a87Christopher Tate                }
2082fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate            }
209efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            return true;
210efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        } catch (IOException e) {
211efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            Log.e(TAG, "Unable to read backup records", e);
212efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor            return false;
2132fdd428e0f18384160f7c38ce3a2cd9ba7e7b2c2Christopher Tate        }
2149bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate    }
2153a31a93b8a195ae2d0180e6dfbf292da2e581f50Christopher Tate
216efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor    public void finishRestore() {
217efe52647f6b41993be43a5f47d1178bb0468cec8Dan Egnor        if (DEBUG) Log.v(TAG, "finishRestore()");
2183a31a93b8a195ae2d0180e6dfbf292da2e581f50Christopher Tate    }
2199bbc21a773cbdfbef2876a75c32bda5839647751Christopher Tate}
220