1f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks/*
2f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * Copyright (C) 2007 The Android Open Source Project
3f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks *
4f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * Licensed under the Apache License, Version 2.0 (the "License");
5f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * you may not use this file except in compliance with the License.
6f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * You may obtain a copy of the License at
7f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks *
8f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks *      http://www.apache.org/licenses/LICENSE-2.0
9f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks *
10f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * Unless required by applicable law or agreed to in writing, software
11f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * distributed under the License is distributed on an "AS IS" BASIS,
12f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * See the License for the specific language governing permissions and
14f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * limitations under the License.
15f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks */
16f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
173c02f2877dc2f8f0b5c01d03fa2b487c040e4000Wei-Ta Chenpackage com.cooliris.media;
183c02f2877dc2f8f0b5c01d03fa2b487c040e4000Wei-Ta Chen
19f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.app.Activity;
20f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.app.ProgressDialog;
21f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.content.Context;
22f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.content.Intent;
23f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.graphics.Bitmap;
24f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.graphics.BitmapFactory;
25f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.net.Uri;
26f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.os.Bundle;
27f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.os.Handler;
28f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.os.Message;
29f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.provider.MediaStore;
30f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport android.util.Log;
31f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
32f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport java.io.File;
33f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport java.io.FileInputStream;
34f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport java.io.FileNotFoundException;
35f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport java.io.IOException;
36f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparksimport java.io.InputStream;
37f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
3829dcedb79b7eb9e3d65adc5d2ea95c1dd5c01df9Chih-Chung Changimport com.cooliris.app.Res;
3929dcedb79b7eb9e3d65adc5d2ea95c1dd5c01df9Chih-Chung Chang
40f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks/**
41f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * Wallpaper picker for the camera application. This just redirects to the
42f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks * standard pick action.
43f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks */
44f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparkspublic class Wallpaper extends Activity {
45f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private static final String LOG_TAG = "Wallpaper";
46f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final int PHOTO_PICKED = 1;
47f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final int CROP_DONE = 2;
48f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
49f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final int SHOW_PROGRESS = 0;
50f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final int FINISH = 1;
51f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
52f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final String DO_LAUNCH_ICICLE = "do_launch";
53f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static final String TEMP_FILE_PATH_ICICLE = "temp_file_path";
54f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
55f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private ProgressDialog mProgressDialog = null;
56f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private boolean mDoLaunch = true;
57f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private File mTempFile;
58f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
59f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private final Handler mHandler = new Handler() {
60f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        @Override
61f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        public void handleMessage(Message msg) {
62f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            switch (msg.what) {
631bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks            case SHOW_PROGRESS: {
6429dcedb79b7eb9e3d65adc5d2ea95c1dd5c01df9Chih-Chung Chang                CharSequence c = getText(Res.string.wallpaper);
651bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                mProgressDialog = ProgressDialog.show(Wallpaper.this, "", c, true, false);
661bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                break;
671bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks            }
681bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks            case FINISH: {
691bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                closeProgressDialog();
701bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                setResult(RESULT_OK);
711bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                finish();
721bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                break;
731bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks            }
74f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            }
75f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
76f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    };
77f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
78f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    static class SetWallpaperThread extends Thread {
79f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        private final Bitmap mBitmap;
80f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        private final Handler mHandler;
81f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        private final Context mContext;
82f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        private final File mFile;
83f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
841bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        public SetWallpaperThread(Bitmap bitmap, Handler handler, Context context, File file) {
85f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mBitmap = bitmap;
86f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mHandler = handler;
87f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mContext = context;
88f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mFile = file;
89f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
90f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
91f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        @Override
92f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        public void run() {
93f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            try {
94f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                mContext.setWallpaper(mBitmap);
95f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            } catch (IOException e) {
96f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                Log.e(LOG_TAG, "Failed to set wallpaper.", e);
97f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            } finally {
98f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                mHandler.sendEmptyMessage(FINISH);
99f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                mFile.delete();
100f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            }
101f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
102f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
103f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
104f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    private synchronized void closeProgressDialog() {
105f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        if (mProgressDialog != null) {
106f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mProgressDialog.dismiss();
107f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mProgressDialog = null;
108f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
109f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
110f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
111f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    @Override
112f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    protected void onCreate(Bundle icicle) {
113f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        super.onCreate(icicle);
114f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        if (icicle != null) {
115f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mDoLaunch = icicle.getBoolean(DO_LAUNCH_ICICLE);
116f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            mTempFile = new File(icicle.getString(TEMP_FILE_PATH_ICICLE));
117f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
118f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
119f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
120f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    @Override
121f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    protected void onSaveInstanceState(Bundle icicle) {
122f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        icicle.putBoolean(DO_LAUNCH_ICICLE, mDoLaunch);
123f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        icicle.putString(TEMP_FILE_PATH_ICICLE, mTempFile.getAbsolutePath());
124f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
125f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
126f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    @Override
127f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    protected void onPause() {
128f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        closeProgressDialog();
129f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        super.onPause();
130f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
131f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
132f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    @Override
133f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    protected void onResume() {
134f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        super.onResume();
135f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        if (!mDoLaunch) {
136f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            return;
137f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
138f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        Uri imageToUse = getIntent().getData();
139f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        if (imageToUse != null) {
140f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            Intent intent = new Intent();
14111f319e713e9f2cd2d1b4293ad028050f8667453Venkat Krishnaraj            intent.setClass(this, CropImage.class);
142f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            intent.setData(imageToUse);
143f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            formatIntent(intent);
144f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            startActivityForResult(intent, CROP_DONE);
145f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        } else {
146f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
147f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            intent.setType("image/*");
148f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            intent.putExtra("crop", "true");
149f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            formatIntent(intent);
150f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            startActivityForResult(intent, PHOTO_PICKED);
151f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
152f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
153f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
154f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    protected void formatIntent(Intent intent) {
155f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // TODO: A temporary file is NOT necessary
156f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // The CropImage intent should be able to set the wallpaper directly
157f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // without writing to a file, which we then need to read here to write
158f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // it again as the final wallpaper, this is silly
159f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        mTempFile = getFileStreamPath("temp-wallpaper");
160f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        mTempFile.getParentFile().mkdirs();
161f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
162f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        int width = getWallpaperDesiredMinimumWidth();
163f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        int height = getWallpaperDesiredMinimumHeight();
1641bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        intent.putExtra("outputX", width);
1651bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        intent.putExtra("outputY", height);
1661bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        intent.putExtra("aspectX", width);
1671bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        intent.putExtra("aspectY", height);
1681bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        intent.putExtra("scale", true);
169f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        intent.putExtra("noFaceDetection", true);
170f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTempFile));
171f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.name());
172f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // TODO: we should have an extra called "setWallpaper" to ask CropImage
173f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // to set the cropped image as a wallpaper directly. This means the
174f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        // SetWallpaperThread should be moved out of this class to CropImage
175f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
176f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks
177f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    @Override
1781bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1791bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks        if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE) && (resultCode == RESULT_OK) && (data != null)) {
180f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            try {
181f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                InputStream s = new FileInputStream(mTempFile);
182f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                try {
183f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    Bitmap bitmap = BitmapFactory.decodeStream(s);
184f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    if (bitmap == null) {
1851bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                        Log.e(LOG_TAG, "Failed to set wallpaper. " + "Couldn't get bitmap for path " + mTempFile);
186f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    } else {
187f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                        mHandler.sendEmptyMessage(SHOW_PROGRESS);
1881bb0c42b2a62f580eea4764d6a4434ffecfbf353Dave Sparks                        new SetWallpaperThread(bitmap, mHandler, this, mTempFile).start();
189f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    }
190f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    mDoLaunch = false;
191f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                } finally {
192f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                    Util.closeSilently(s);
193f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                }
194f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            } catch (FileNotFoundException ex) {
195f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks                Log.e(LOG_TAG, "file not found: " + mTempFile, ex);
196f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            }
197f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        } else {
198f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            setResult(RESULT_CANCELED);
199f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks            finish();
200f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks        }
201f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks    }
202f99dfe8549fb6c2c06c8cb7ca7d5eb33002c809eDave Sparks}
203