PageRenderTime 43ms CodeModel.GetById 17ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/Magnifier/src/com/ideal/magnifier/MagnifierActivity.java

http://eyes-free.googlecode.com/
Java | 581 lines | 363 code | 51 blank | 167 comment | 48 complexity | 2c08e988b2f33077cf4fced0780d2992 MD5 | raw file
  1/*
  2 * Copyright (C) 2010 The IDEAL Group
  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.ideal.magnifier;
 18
 19import android.app.Activity;
 20import android.app.AlertDialog;
 21import android.content.ActivityNotFoundException;
 22import android.content.Context;
 23import android.content.DialogInterface;
 24import android.content.Intent;
 25import android.content.SharedPreferences;
 26import android.graphics.Bitmap;
 27import android.graphics.BitmapFactory;
 28import android.graphics.ImageFormat;
 29import android.graphics.Rect;
 30import android.graphics.YuvImage;
 31import android.hardware.Camera;
 32import android.hardware.Camera.AutoFocusCallback;
 33import android.hardware.Camera.Parameters;
 34import android.hardware.Camera.PreviewCallback;
 35import android.net.Uri;
 36import android.os.Bundle;
 37import android.view.Display;
 38import android.view.KeyEvent;
 39import android.view.Menu;
 40import android.view.MenuItem;
 41import android.view.MotionEvent;
 42import android.view.SurfaceHolder;
 43import android.view.SurfaceHolder.Callback;
 44import android.view.SurfaceView;
 45import android.view.View;
 46import android.view.View.OnTouchListener;
 47import android.widget.ImageView;
 48import android.widget.LinearLayout;
 49
 50import java.io.ByteArrayOutputStream;
 51import java.io.IOException;
 52import java.util.List;
 53
 54/**
 55 * Main activity for IDEAL Magnifier. Uses the phone's camera to turn Android
 56 * into a video magnifier. Volume buttons to zoom in/out Search button to turn
 57 * on the LED light Menu to bring up the color filter options If the image is
 58 * blurry, just tap the screen and it will refocus.
 59 */
 60public class MagnifierActivity extends Activity implements Callback {
 61
 62    /**
 63     * The user interaction cool down period in milliseconds. The camera's
 64     * autoFocus method is invoked after this delay if no further user
 65     * interactions occur.
 66     */
 67    public static final long FOCUS_INTERACTION_TIMEOUT_THRESHOLD = 750;
 68
 69    /**
 70     * The threshold time for which a finger must remain on the
 71     * MagnifiedImageView before it is considered a long click and initiates a
 72     * pause toggle event.
 73     */
 74    public static final long LONG_PRESS_INTERACTION_THRESHOLD = 1500;
 75
 76    /**
 77     * Runnable used to defer camera focusing until user interaction has stopped
 78     * for FOCUS_INTERACTION_TIMEOUT_THRESHOLD milliseconds.
 79     */
 80    private Runnable mFocuserLocked = null;
 81
 82    /**
 83     * Runnable used to toggle the paused state of the MagnifiedImageView
 84     */
 85    private Runnable mPauser = null;
 86
 87    /**
 88     * A flag indicating whether the camera is currently performing an
 89     * auto-focus operation.
 90     */
 91    private boolean mCameraFocusing = false;
 92
 93    /**
 94     * A flag indicating whether or not a user interaction occurred during the
 95     * last camera focus event. Used to defer updating of camera parameters
 96     * until the focus operation completes.
 97     */
 98    private boolean mParameterSettingDeferred = false;
 99
100    /**
101     * The last set of camera parameters that was set successfully. Used to
102     * return the camera to its last known state when returning from a paused
103     * state.
104     */
105    private Parameters mLastCameraParameters = null;
106
107    /**
108     * The set of camera parameters to be applied after a focus operation
109     * completes.
110     */
111    private Parameters mDeferredParameters = null;
112
113    /**
114     * The Layout Manager holding either the MagnificationView or
115     * MagnifiedImageView
116     */
117    private LinearLayout mRootView = null;
118
119    /**
120     * Surface used to project the camera preview.
121     */
122    private SurfaceHolder mHolder = null;
123
124    /**
125     * Abstracted SurfaceView used to further magnify the camera preview frames.
126     */
127    private MagnificationView mPreview = null;
128
129    /**
130     * Abstracted ImageView used to further magnify a single camera preview
131     * frame.
132     */
133    private MagnifiedImageView mImagePreview = null;
134
135    /**
136     * The system's camera hardware
137     */
138    private Camera mCamera = null;
139
140    /**
141     * Handler for touch events. Used for focusing and pausing the magnifier.
142     */
143    private OnTouchListener mTouchListener = null;
144
145    /**
146     * Callback used to obtain camera data for pausing the magnifier.
147     */
148    private PreviewCallback mPreviewCallback = null;
149
150    /**
151     * The current zoom level
152     */
153    private int mZoom = 0;
154
155    /**
156     * The current camera mode
157     */
158    private String mCameraMode = null;
159
160    /**
161     * Flag indicating the current state of the camera flash LED.
162     */
163    private boolean mTorch = false;
164
165    /**
166     * Flag indicating the state of the surface, true indicating paused.
167     */
168    private boolean mMagnifierPaused = false;
169
170    /**
171     * The Preferences in which we store the last application state.
172     */
173    private SharedPreferences mPrefs = null;
174
175    /**
176     * These classes lie about our actual screen size, thus giving us a 2x
177     * digital zoom to start with even before we invoke the hardware zoom
178     * features of the camera.
179     */
180    public class MagnificationView extends SurfaceView {
181        public MagnificationView(Context context) {
182            super(context);
183        }
184
185        @Override
186        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
187            // TODO: This method miscalculates the dimensions of the projected
188            // screen, causing a horizontal stretch effect.
189            Display display = getWindowManager().getDefaultDisplay();
190            int width = display.getWidth();
191            int height = display.getHeight();
192            if (width * height > 643200) {
193                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
194            } else {
195                // TODO: We short-circuit the onMeasure if we have a screen size
196                // above a certain threshold. This keeps the system from silently
197                // killing our app for memory usage.
198                widthMeasureSpec = MeasureSpec.makeMeasureSpec(width * 2, MeasureSpec.EXACTLY);
199                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height * 2, MeasureSpec.EXACTLY);
200                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
201            }
202        }
203    }
204
205    public class MagnifiedImageView extends ImageView {
206        public MagnifiedImageView(Context context) {
207            super(context);
208        }
209
210        @Override
211        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
212            // TODO: This method miscalculates the dimensions of the projected
213            // screen, causing a horizontal stretch effect.
214            Display display = getWindowManager().getDefaultDisplay();
215            int width = display.getWidth();
216            int height = display.getHeight();
217            if (width * height > 643200) {
218                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
219            } else {
220                // TODO: We short-circuit the onMeasure if we have a screen size
221                // above a certain threshold. This keeps the system from silently
222                // killing our app for memory usage.
223                widthMeasureSpec = MeasureSpec.makeMeasureSpec(width * 2, MeasureSpec.EXACTLY);
224                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height * 2, MeasureSpec.EXACTLY);
225                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
226            }
227        }
228    }
229
230    /** Called when the activity is first created. */
231    @Override
232    public void onCreate(Bundle savedInstanceState) {
233        super.onCreate(savedInstanceState);
234
235        setContentView(R.layout.main);
236        mPreview = new MagnificationView(this);
237        mImagePreview = new MagnifiedImageView(this);
238        mPauser = new Runnable() {
239            @Override
240            public void run() {
241                togglePausePreview(true);
242            }
243        };
244        mTouchListener = new OnTouchListener() {
245            @Override
246            public boolean onTouch(View view, MotionEvent event) {
247                switch (event.getAction()) {
248                    case MotionEvent.ACTION_DOWN:
249                        mFocuserLocked.run();
250                        mPreview.postDelayed(mPauser, LONG_PRESS_INTERACTION_THRESHOLD);
251                        return true;
252                    case MotionEvent.ACTION_UP:
253                    case MotionEvent.ACTION_CANCEL:
254                        mPreview.removeCallbacks(mPauser);
255                        togglePausePreview(false);
256                        return true;
257                }
258
259                return false;
260            }
261        };
262        mPreview.setOnTouchListener(mTouchListener);
263        mImagePreview.setOnTouchListener(mTouchListener);
264        mPreviewCallback = new PreviewCallback() {
265            @Override
266            /**
267             * This callback will be used to capture a single frame from the camera
268             * hardware.  We process the image in YUV format, convert to JPEG, and
269             * finally to Bitmap so it can be shown in an extended ImageView.  This
270             * process is sub-optimal as Android's graphics BitmapFactory does not
271             * support direct YUV to Bitmap conversions.
272             */
273            public void onPreviewFrame(byte[] data, Camera camera) {
274                Camera.Parameters parameters = camera.getParameters();
275
276                Camera.Size size = parameters.getPreviewSize();
277                if (size == null) {
278                    // We've failed to get preview frame data, so switch pack to
279                    // a live view.
280                    togglePausePreview(false);
281                    return;
282                }
283
284                // Generate a YuvImage from the camera data
285                int w = parameters.getPreviewSize().width;
286                int h = parameters.getPreviewSize().height;
287                YuvImage pausedYuvImage = new YuvImage(data, parameters.getPreviewFormat(), w, h,
288                        null);
289
290                // Compress the YuvImage to JPEG Output Stream
291                ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
292                pausedYuvImage.compressToJpeg(new Rect(0, 0, w, h), 100, jpegOutputStream);
293
294                // Use BitmapFactory to create a Bitmap from the JPEG stream
295                Bitmap pausedImage = BitmapFactory.decodeByteArray(jpegOutputStream.toByteArray(),
296                        0, jpegOutputStream.size());
297
298                // Scale the Bitmap to match the MagnifiedImageView's dimensions
299                Display display = getWindowManager().getDefaultDisplay();
300                int width = display.getWidth();
301                int height = display.getHeight();
302                mImagePreview.setImageBitmap(Bitmap.createScaledBitmap(pausedImage, width * 2,
303                        height * 2, false));
304                mRootView.removeAllViews();
305                mRootView.addView(mImagePreview);
306            }
307        };
308        mRootView = (LinearLayout) findViewById(R.id.rootView);
309        mRootView.addView(mPreview);
310        mPreview.setKeepScreenOn(true);
311        mHolder = mPreview.getHolder();
312        mHolder.addCallback(this);
313        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
314        mFocuserLocked = new Runnable() {
315            @Override
316            public void run() {
317                // This Runnable will focus the camera, and guarantee that calls
318                // to Camera.autoFocus are synchronous.
319                if (mCamera != null && !mCameraFocusing) {
320                    mCameraFocusing = true;
321                    mCamera.autoFocus(new AutoFocusCallback() {
322                        @Override
323                        public void onAutoFocus(boolean success, Camera camera) {
324                            mCameraFocusing = false;
325                            if (mParameterSettingDeferred) {
326                                // The user attempted to change a camera
327                                // parameter during the focus event, set the
328                                // parameters again so the user's change
329                                // takes effect.
330                                setParams(mDeferredParameters);
331                                mParameterSettingDeferred = false;
332                            }
333                        }
334                    });
335                }
336            }
337        };
338    }
339
340    @Override
341    public boolean onKeyDown(int keyCode, KeyEvent event) {
342        if (mCamera != null) {
343            Parameters params = mCamera.getParameters();
344            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
345                mZoom = mZoom + 1;
346                if (mZoom > params.getMaxZoom()) {
347                    mZoom = params.getMaxZoom();
348                }
349                params.setZoom(mZoom);
350                setParams(params);
351                return true;
352            }
353            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
354                mZoom = mZoom - 1;
355                if (mZoom < 0) {
356                    mZoom = 0;
357                }
358                params.setZoom(mZoom);
359                setParams(params);
360                return true;
361            }
362            if (keyCode == KeyEvent.KEYCODE_SEARCH) {
363                if (mTorch) {
364                    params.setFlashMode(Parameters.FLASH_MODE_OFF);
365                    mTorch = false;
366                } else {
367                    params.setFlashMode(Parameters.FLASH_MODE_TORCH);
368                    mTorch = true;
369                }
370                setParams(params);
371                return true;
372            }
373        }
374        if (keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ENTER
375                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
376                || (keyCode == KeyEvent.KEYCODE_BACK && mMagnifierPaused)) {
377            togglePausePreview(!mMagnifierPaused);
378            return true;
379        }
380        return super.onKeyDown(keyCode, event);
381    }
382
383    private void setParams(Parameters params) {
384        // Setting the parameters of the camera is only a safe operation if the
385        // camera is not presently focusing. Only focus the camera after the
386        // user interaction has quieted down, verified by a delay period.
387        mPreview.removeCallbacks(mFocuserLocked);
388        if (!mCameraFocusing) {
389            // On some phones such as the Motorola Droid, the preview needs to
390            // be stopped and then restarted.
391            // Failing to do so will result in an ANR (application not
392            // responding) error.
393            //
394            // TODO: Check if it is possible to detect when a restart is needed
395            // by checking isSmoothZoomSupported in the Camera Parameters.
396            //
397            // Nexus One: False (does not need a restart)
398            // Motorola Droid: True (must have preview restarted)
399            //
400            // Log.e("smooth zoom?", params.isSmoothZoomSupported() + "");
401            mCamera.stopPreview();
402            mCamera.startPreview();
403            mCamera.setParameters(params);
404            mLastCameraParameters = params;
405        } else {
406            mParameterSettingDeferred = true;
407            mDeferredParameters = params;
408        }
409        mPreview.postDelayed(mFocuserLocked, FOCUS_INTERACTION_TIMEOUT_THRESHOLD);
410    }
411
412    @Override
413    public void surfaceCreated(SurfaceHolder holder) {
414        if (mCamera == null) {
415            mCamera = Camera.open();
416        }
417        mCameraFocusing = false;
418        try {
419            mCamera.setPreviewDisplay(holder);
420        } catch (IOException e) {
421            e.printStackTrace();
422        }
423        Parameters initParams;
424        if (mLastCameraParameters != null) {
425            initParams = mLastCameraParameters;
426        } else {
427            initParams = mCamera.getParameters();
428            mLastCameraParameters = initParams;
429        }
430        initParams.setPreviewFormat(ImageFormat.NV21);
431        setParams(initParams);
432    }
433
434    @Override
435    public void surfaceDestroyed(SurfaceHolder holder) {
436        mCamera.stopPreview();
437        mCamera.release();
438        mCamera = null;
439    }
440
441    @Override
442    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
443        mHolder.setFixedSize(w, h);
444
445        mCamera.startPreview();
446
447        // Note: Parameters are not safe to set until AFTER the preview is up.
448        // One some phones, it is OK (such as the Nexus One), but on others,
449        // (such as the Motorola Droid), this will cause the parameters to not
450        // actually change/may lead to an ANR on a subsequent set attempt.
451        Parameters params = mCamera.getParameters();
452
453        // Reload the previous state from the stored preferences.
454        mPrefs = getSharedPreferences(getString(R.string.app_name), 0);
455        mZoom = mPrefs.getInt(getString(R.string.zoom_level_pref), mCamera.getParameters()
456                .getMaxZoom() / 2);
457        mCameraMode = mPrefs.getString(getString(R.string.camera_mode_pref), mCamera
458                .getParameters().getSupportedColorEffects().get(0));
459        params.setZoom(mZoom);
460        params.setColorEffect(mCameraMode);
461        setParams(params);
462    }
463
464    public void showEffectsList() {
465        if (mCamera == null) {
466            return;
467        }
468
469        List<String> effectsList = mCamera.getParameters().getSupportedColorEffects();
470        String[] effects = {
471                ""
472        };
473        effects = effectsList.toArray(effects);
474        final String[] items = effects;
475        AlertDialog.Builder builder = new AlertDialog.Builder(this);
476        builder.setTitle(getString(R.string.color_effect_dialog_title));
477        builder.setItems(items, new DialogInterface.OnClickListener() {
478            @Override
479            public void onClick(DialogInterface dialog, int item) {
480                Parameters params = mCamera.getParameters();
481                mCameraMode = items[item];
482                params.setColorEffect(items[item]);
483                setParams(params);
484            }
485        });
486        builder.create().show();
487    }
488
489    @Override
490    public boolean onCreateOptionsMenu(Menu menu) {
491        super.onCreateOptionsMenu(menu);
492        // Parameters for menu.add are:
493        // group -- Not used here.
494        // id -- Used only when you want to handle and identify the click
495        // yourself.
496        // title
497        menu.add(0, 0, 0, getString(R.string.color_effect_button_text)).setIcon(
498                android.R.drawable.ic_menu_manage);
499        menu.add(0, 1, 0, getString(R.string.toggle_freeze_frame_button_text)).setIcon(
500                android.R.drawable.ic_menu_camera);
501        menu.add(0, 2, 0, getString(R.string.toggle_light_button_text)).setIcon(
502                android.R.drawable.ic_menu_view);
503        menu.add(0, 3, 0, getString(R.string.more_apps_button_text)).setIcon(
504                android.R.drawable.ic_menu_search);
505        return true;
506    }
507
508    /**
509     * Activity callback that lets your handle the selection in the class.
510     * Return true to indicate that you've got it, false to indicate that it
511     * should be handled by a declared handler object for that item (handler
512     * objects are discouraged for reasons of efficiency).
513     */
514    @Override
515    public boolean onOptionsItemSelected(MenuItem item) {
516        switch (item.getItemId()) {
517            case 0:
518                showEffectsList();
519                return true;
520            case 1:
521                togglePausePreview(!mMagnifierPaused);
522                return true;
523            case 2:
524                Parameters params = mCamera.getParameters();
525                if (mTorch) {
526                    params.setFlashMode(Parameters.FLASH_MODE_OFF);
527                    mTorch = false;
528                } else {
529                    params.setFlashMode(Parameters.FLASH_MODE_TORCH);
530                    mTorch = true;
531                }
532                setParams(params);
533                break;
534            case 3:
535                String marketUrl = "market://search?q=pub:\"IDEAL Group, Inc. Android Development Team\"";
536                Intent i = new Intent(Intent.ACTION_VIEW);
537                i.setData(Uri.parse(marketUrl));
538                try {
539                    startActivity(i);
540                } catch (ActivityNotFoundException anf) {
541                    new AlertDialog.Builder(this)
542                            .setTitle(getString(R.string.market_launch_error_title))
543                            .setMessage(getString(R.string.market_launch_error_text))
544                            .setNeutralButton(getString(R.string.market_launch_error_button), null)
545                            .show();
546                }
547                return true;
548        }
549        return false;
550    }
551
552    /**
553     * Toggles the freeze frame feature by removing the MagnificationView and
554     * adding a MagnifiedImageView with a single scaled preview frame.
555     */
556    public synchronized void togglePausePreview(boolean shouldPause) {
557        if (mMagnifierPaused == shouldPause) {
558            return;
559        }
560        mMagnifierPaused = shouldPause;
561        if (!mMagnifierPaused) {
562            mRootView.removeAllViews();
563            mRootView.addView(mPreview);
564        } else {
565            mCamera.setOneShotPreviewCallback(mPreviewCallback);
566        }
567    }
568
569    /**
570     * Save the state of the application as it loses focus.
571     */
572    @Override
573    public void onPause() {
574        super.onPause();
575
576        SharedPreferences.Editor editor = mPrefs.edit();
577        editor.putInt(getString(R.string.zoom_level_pref), mZoom);
578        editor.putString(getString(R.string.camera_mode_pref), mCameraMode);
579        editor.commit();
580    }
581}