/Magnifier/src/com/ideal/magnifier/MagnifierActivity.java
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}