Credit Ryan
[zxing.git] / android / src / com / google / zxing / client / android / CaptureActivity.java
1 /*
2  * Copyright (C) 2008 ZXing authors
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
17 package com.google.zxing.client.android;
18
19 import com.google.zxing.BarcodeFormat;
20 import com.google.zxing.Result;
21 import com.google.zxing.ResultPoint;
22 import com.google.zxing.client.android.history.HistoryManager;
23 import com.google.zxing.client.android.result.ResultButtonListener;
24 import com.google.zxing.client.android.result.ResultHandler;
25 import com.google.zxing.client.android.result.ResultHandlerFactory;
26 import com.google.zxing.client.android.share.ShareActivity;
27
28 import android.app.Activity;
29 import android.app.AlertDialog;
30 import android.content.DialogInterface;
31 import android.content.Intent;
32 import android.content.SharedPreferences;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.content.res.AssetFileDescriptor;
36 import android.content.res.Configuration;
37 import android.graphics.Bitmap;
38 import android.graphics.Canvas;
39 import android.graphics.Paint;
40 import android.graphics.Rect;
41 import android.media.AudioManager;
42 import android.media.MediaPlayer;
43 import android.media.MediaPlayer.OnCompletionListener;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.Message;
48 import android.os.Vibrator;
49 import android.preference.PreferenceManager;
50 import android.text.ClipboardManager;
51 import android.text.SpannableStringBuilder;
52 import android.text.style.UnderlineSpan;
53 import android.util.Log;
54 import android.view.Gravity;
55 import android.view.KeyEvent;
56 import android.view.Menu;
57 import android.view.MenuItem;
58 import android.view.SurfaceHolder;
59 import android.view.SurfaceView;
60 import android.view.View;
61 import android.view.ViewGroup;
62 import android.view.Window;
63 import android.view.WindowManager;
64 import android.widget.ImageView;
65 import android.widget.TextView;
66
67 import java.io.IOException;
68 import java.text.DateFormat;
69 import java.util.Arrays;
70 import java.util.List;
71 import java.util.Date;
72 import java.util.Vector;
73 import java.util.regex.Pattern;
74
75 /**
76  * The barcode reader activity itself. This is loosely based on the CameraPreview
77  * example included in the Android SDK.
78  *
79  * @author dswitkin@google.com (Daniel Switkin)
80  */
81 public final class CaptureActivity extends Activity implements SurfaceHolder.Callback {
82
83   private static final String TAG = CaptureActivity.class.getSimpleName();
84
85   private static final Pattern COMMA_PATTERN = Pattern.compile(",");
86
87   private static final int SHARE_ID = Menu.FIRST;
88   private static final int HISTORY_ID = Menu.FIRST + 1;
89   private static final int SETTINGS_ID = Menu.FIRST + 2;
90   private static final int HELP_ID = Menu.FIRST + 3;
91   private static final int ABOUT_ID = Menu.FIRST + 4;
92
93   private static final long INTENT_RESULT_DURATION = 1500L;
94   private static final float BEEP_VOLUME = 0.10f;
95   private static final long VIBRATE_DURATION = 200L;
96
97   private static final String PACKAGE_NAME = "com.google.zxing.client.android";
98   private static final String PRODUCT_SEARCH_URL_PREFIX = "http://www.google";
99   private static final String PRODUCT_SEARCH_URL_SUFFIX = "/m/products/scan";
100   private static final String ZXING_URL = "http://zxing.appspot.com/scan";
101   private static final String RETURN_CODE_PLACEHOLDER = "{CODE}";
102   private static final String RETURN_URL_PARAM = "ret";
103
104   static final Vector<BarcodeFormat> PRODUCT_FORMATS;
105   static final Vector<BarcodeFormat> ONE_D_FORMATS;
106   static final Vector<BarcodeFormat> QR_CODE_FORMATS;
107   static final Vector<BarcodeFormat> ALL_FORMATS;
108
109   static {
110     PRODUCT_FORMATS = new Vector<BarcodeFormat>(5);
111     PRODUCT_FORMATS.add(BarcodeFormat.UPC_A);
112     PRODUCT_FORMATS.add(BarcodeFormat.UPC_E);
113     PRODUCT_FORMATS.add(BarcodeFormat.EAN_13);
114     PRODUCT_FORMATS.add(BarcodeFormat.EAN_8);
115     //PRODUCT_FORMATS.add(BarcodeFormat.RSS14);
116     ONE_D_FORMATS = new Vector<BarcodeFormat>(PRODUCT_FORMATS.size() + 3);
117     ONE_D_FORMATS.addAll(PRODUCT_FORMATS);
118     ONE_D_FORMATS.add(BarcodeFormat.CODE_39);
119     ONE_D_FORMATS.add(BarcodeFormat.CODE_128);
120     ONE_D_FORMATS.add(BarcodeFormat.ITF);
121     QR_CODE_FORMATS = new Vector<BarcodeFormat>(1);
122     QR_CODE_FORMATS.add(BarcodeFormat.QR_CODE);
123     ALL_FORMATS = new Vector<BarcodeFormat>(ONE_D_FORMATS.size() + QR_CODE_FORMATS.size());
124     ALL_FORMATS.addAll(ONE_D_FORMATS);
125     ALL_FORMATS.addAll(QR_CODE_FORMATS);
126   }
127
128   private enum Source {
129     NATIVE_APP_INTENT,
130     PRODUCT_SEARCH_LINK,
131     ZXING_LINK,
132     NONE
133   }
134
135   private CaptureActivityHandler handler;
136
137   private ViewfinderView viewfinderView;
138   private View statusView;
139   private View resultView;
140   private MediaPlayer mediaPlayer;
141   private Result lastResult;
142   private boolean hasSurface;
143   private boolean playBeep;
144   private boolean vibrate;
145   private boolean copyToClipboard;
146   private Source source;
147   private String sourceUrl;
148   private String returnUrlTemplate;
149   private Vector<BarcodeFormat> decodeFormats;
150   private String characterSet;
151   private String versionName;
152   private HistoryManager historyManager;
153
154   private final OnCompletionListener beepListener = new BeepListener();
155
156   private final DialogInterface.OnClickListener aboutListener =
157       new DialogInterface.OnClickListener() {
158     public void onClick(DialogInterface dialogInterface, int i) {
159       Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.zxing_url)));
160       intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
161       startActivity(intent);
162     }
163   };
164
165   ViewfinderView getViewfinderView() {
166     return viewfinderView;
167   }
168
169   public Handler getHandler() {
170     return handler;
171   }
172
173   @Override
174   public void onCreate(Bundle icicle) {
175     super.onCreate(icicle);
176
177     Window window = getWindow();
178     window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
179     setContentView(R.layout.capture);
180
181     CameraManager.init(getApplication());
182     viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
183     resultView = findViewById(R.id.result_view);
184     statusView = findViewById(R.id.status_view);
185     handler = null;
186     lastResult = null;
187     hasSurface = false;
188     historyManager = new HistoryManager(this);
189     historyManager.trimHistory();
190
191     showHelpOnFirstLaunch();
192   }
193
194   @Override
195   protected void onResume() {
196     super.onResume();
197
198     SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
199     SurfaceHolder surfaceHolder = surfaceView.getHolder();
200     if (hasSurface) {
201       // The activity was paused but not stopped, so the surface still exists. Therefore
202       // surfaceCreated() won't be called, so init the camera here.
203       initCamera(surfaceHolder);
204     } else {
205       // Install the callback and wait for surfaceCreated() to init the camera.
206       surfaceHolder.addCallback(this);
207       surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
208     }
209
210     Intent intent = getIntent();
211     String action = intent == null ? null : intent.getAction();
212     String dataString = intent == null ? null : intent.getDataString();
213     if (intent != null && action != null) {
214       if (action.equals(Intents.Scan.ACTION)) {
215         // Scan the formats the intent requested, and return the result to the calling activity.
216         source = Source.NATIVE_APP_INTENT;
217         decodeFormats = parseDecodeFormats(intent);
218         resetStatusView();
219       } else if (dataString != null && dataString.contains(PRODUCT_SEARCH_URL_PREFIX) &&
220           dataString.contains(PRODUCT_SEARCH_URL_SUFFIX)) {
221         // Scan only products and send the result to mobile Product Search.
222         source = Source.PRODUCT_SEARCH_LINK;
223         sourceUrl = dataString;
224         decodeFormats = PRODUCT_FORMATS;
225         resetStatusView();
226       } else if (dataString != null && dataString.startsWith(ZXING_URL)) {
227         // Scan formats requested in query string (all formats if none specified).
228         // If a return URL is specified, send the results there. Otherwise, handle the results ourselves.
229         source = Source.ZXING_LINK;
230         sourceUrl = dataString;
231         Uri inputUri = Uri.parse(sourceUrl);
232         returnUrlTemplate = inputUri.getQueryParameter(RETURN_URL_PARAM);
233         decodeFormats = parseDecodeFormats(inputUri);
234         resetStatusView();
235       } else {
236         // Scan all formats and handle the results ourselves (launched from Home).
237         source = Source.NONE;
238         decodeFormats = null;
239         resetStatusView();
240       }
241       characterSet = intent.getStringExtra(Intents.Scan.CHARACTER_SET);
242     } else {
243       source = Source.NONE;
244       decodeFormats = null;
245       characterSet = null;
246       if (lastResult == null) {
247         resetStatusView();
248       }
249     }
250
251     SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
252     playBeep = prefs.getBoolean(PreferencesActivity.KEY_PLAY_BEEP, true);
253     vibrate = prefs.getBoolean(PreferencesActivity.KEY_VIBRATE, false);
254     copyToClipboard = prefs.getBoolean(PreferencesActivity.KEY_COPY_TO_CLIPBOARD, true);
255     initBeepSound();
256   }
257
258   private static Vector<BarcodeFormat> parseDecodeFormats(Intent intent) {
259     return parseDecodeFormats(
260         Arrays.asList(COMMA_PATTERN.split(intent.getStringExtra(Intents.Scan.SCAN_FORMATS))),
261         intent.getStringExtra(Intents.Scan.MODE));
262   }
263   
264   private static Vector<BarcodeFormat> parseDecodeFormats(Uri inputUri) {
265     List<String> formats = inputUri.getQueryParameters(Intents.Scan.SCAN_FORMATS);
266     if (formats.size() == 1){
267       formats = Arrays.asList(COMMA_PATTERN.split(formats.get(0)));
268     }
269     return parseDecodeFormats(formats, inputUri.getQueryParameter(Intents.Scan.MODE));
270   }
271   
272   private static Vector<BarcodeFormat> parseDecodeFormats(List<String> scanFormats,
273                                                           String decodeMode) {
274     if (scanFormats != null) {
275       Vector<BarcodeFormat> formats = new Vector<BarcodeFormat>();
276       try {
277         for (String format : scanFormats) {
278           formats.add(BarcodeFormat.valueOf(format));
279         }
280         return formats;
281       } catch (IllegalArgumentException iae) {
282         // ignore it then
283       }
284     }
285     if (decodeMode != null) {
286       if (Intents.Scan.PRODUCT_MODE.equals(decodeMode)) {
287         return PRODUCT_FORMATS;
288       }
289       if (Intents.Scan.QR_CODE_MODE.equals(decodeMode)) {
290         return QR_CODE_FORMATS;
291       }
292       if (Intents.Scan.ONE_D_MODE.equals(decodeMode)) {
293         return ONE_D_FORMATS;
294       }
295     }
296     return null;
297   }
298
299   @Override
300   protected void onPause() {
301     super.onPause();
302     if (handler != null) {
303       handler.quitSynchronously();
304       handler = null;
305     }
306     CameraManager.get().closeDriver();
307   }
308
309   @Override
310   public boolean onKeyDown(int keyCode, KeyEvent event) {
311     if (keyCode == KeyEvent.KEYCODE_BACK) {
312       if (source == Source.NATIVE_APP_INTENT) {
313         setResult(RESULT_CANCELED);
314         finish();
315         return true;
316       } else if ((source == Source.NONE || source == Source.ZXING_LINK) && lastResult != null) {
317         resetStatusView();
318         if (handler != null) {
319           handler.sendEmptyMessage(R.id.restart_preview);
320         }
321         return true;
322       }
323     } else if (keyCode == KeyEvent.KEYCODE_FOCUS || keyCode == KeyEvent.KEYCODE_CAMERA) {
324       // Handle these events so they don't launch the Camera app
325       return true;
326     }
327     return super.onKeyDown(keyCode, event);
328   }
329
330   @Override
331   public boolean onCreateOptionsMenu(Menu menu) {
332     super.onCreateOptionsMenu(menu);
333     menu.add(0, SHARE_ID, 0, R.string.menu_share)
334         .setIcon(android.R.drawable.ic_menu_share);
335     menu.add(0, HISTORY_ID, 0, R.string.menu_history)
336         .setIcon(android.R.drawable.ic_menu_recent_history);
337     menu.add(0, SETTINGS_ID, 0, R.string.menu_settings)
338         .setIcon(android.R.drawable.ic_menu_preferences);
339     menu.add(0, HELP_ID, 0, R.string.menu_help)
340         .setIcon(android.R.drawable.ic_menu_help);
341     menu.add(0, ABOUT_ID, 0, R.string.menu_about)
342         .setIcon(android.R.drawable.ic_menu_info_details);
343     return true;
344   }
345
346   // Don't display the share menu item if the result overlay is showing.
347   @Override
348   public boolean onPrepareOptionsMenu(Menu menu) {
349     super.onPrepareOptionsMenu(menu);
350     menu.findItem(SHARE_ID).setVisible(lastResult == null);
351     return true;
352   }
353
354   @Override
355   public boolean onOptionsItemSelected(MenuItem item) {
356     switch (item.getItemId()) {
357       case SHARE_ID: {
358         Intent intent = new Intent(Intent.ACTION_VIEW);
359         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
360         intent.setClassName(this, ShareActivity.class.getName());
361         startActivity(intent);
362         break;
363       }
364       case HISTORY_ID: {
365         AlertDialog historyAlert = historyManager.buildAlert();
366         historyAlert.show();
367         break;
368       }
369       case SETTINGS_ID: {
370         Intent intent = new Intent(Intent.ACTION_VIEW);
371         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
372         intent.setClassName(this, PreferencesActivity.class.getName());
373         startActivity(intent);
374         break;
375       }
376       case HELP_ID: {
377         Intent intent = new Intent(Intent.ACTION_VIEW);
378         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
379         intent.setClassName(this, HelpActivity.class.getName());
380         startActivity(intent);
381         break;
382       }
383       case ABOUT_ID:
384         AlertDialog.Builder builder = new AlertDialog.Builder(this);
385         builder.setTitle(getString(R.string.title_about) + versionName);
386         builder.setMessage(getString(R.string.msg_about) + "\n\n" + getString(R.string.zxing_url));
387         builder.setIcon(R.drawable.zxing_icon);
388         builder.setPositiveButton(R.string.button_open_browser, aboutListener);
389         builder.setNegativeButton(R.string.button_cancel, null);
390         builder.show();
391         break;
392     }
393     return super.onOptionsItemSelected(item);
394   }
395
396   @Override
397   public void onConfigurationChanged(Configuration config) {
398     // Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
399     super.onConfigurationChanged(config);
400   }
401
402   public void surfaceCreated(SurfaceHolder holder) {
403     if (!hasSurface) {
404       hasSurface = true;
405       initCamera(holder);
406     }
407   }
408
409   public void surfaceDestroyed(SurfaceHolder holder) {
410     hasSurface = false;
411   }
412
413   public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
414
415   }
416
417   /**
418    * A valid barcode has been found, so give an indication of success and show the results.
419    *
420    * @param rawResult The contents of the barcode.
421    * @param barcode   A greyscale bitmap of the camera data which was decoded.
422    */
423   public void handleDecode(Result rawResult, Bitmap barcode) {
424     lastResult = rawResult;
425     historyManager.addHistoryItem(rawResult);
426     if (barcode == null) {
427       // This is from history -- no saved barcode
428       handleDecodeInternally(rawResult, null);
429     } else {
430       playBeepSoundAndVibrate();
431       drawResultPoints(barcode, rawResult);
432       switch (source) {
433         case NATIVE_APP_INTENT:
434         case PRODUCT_SEARCH_LINK:
435           handleDecodeExternally(rawResult, barcode);
436           break;
437         case ZXING_LINK:
438           if(returnUrlTemplate == null){
439             handleDecodeInternally(rawResult, barcode);
440           } else {
441             handleDecodeExternally(rawResult, barcode);
442           }
443           break;
444         case NONE:
445           handleDecodeInternally(rawResult, barcode);
446           break;
447       }
448     }
449   }
450
451   /**
452    * Superimpose a line for 1D or dots for 2D to highlight the key features of the barcode.
453    *
454    * @param barcode   A bitmap of the captured image.
455    * @param rawResult The decoded results which contains the points to draw.
456    */
457   private void drawResultPoints(Bitmap barcode, Result rawResult) {
458     ResultPoint[] points = rawResult.getResultPoints();
459     if (points != null && points.length > 0) {
460       Canvas canvas = new Canvas(barcode);
461       Paint paint = new Paint();
462       paint.setColor(getResources().getColor(R.color.result_image_border));
463       paint.setStrokeWidth(3.0f);
464       paint.setStyle(Paint.Style.STROKE);
465       Rect border = new Rect(2, 2, barcode.getWidth() - 2, barcode.getHeight() - 2);
466       canvas.drawRect(border, paint);
467
468       paint.setColor(getResources().getColor(R.color.result_points));
469       if (points.length == 2) {
470         paint.setStrokeWidth(4.0f);
471         canvas.drawLine(points[0].getX(), points[0].getY(), points[1].getX(),
472             points[1].getY(), paint);
473       } else {
474         paint.setStrokeWidth(10.0f);
475         for (ResultPoint point : points) {
476           canvas.drawPoint(point.getX(), point.getY(), paint);
477         }
478       }
479     }
480   }
481
482   // Put up our own UI for how to handle the decoded contents.
483   private void handleDecodeInternally(Result rawResult, Bitmap barcode) {
484     statusView.setVisibility(View.GONE);
485     viewfinderView.setVisibility(View.GONE);
486     resultView.setVisibility(View.VISIBLE);
487
488     ImageView barcodeImageView = (ImageView) findViewById(R.id.barcode_image_view);
489     if (barcode == null) {
490       barcodeImageView.setImageResource(R.drawable.zxing_icon);
491     } else {
492       barcodeImageView.setImageBitmap(barcode);
493     }
494     barcodeImageView.setVisibility(View.VISIBLE);
495
496     TextView formatTextView = (TextView) findViewById(R.id.format_text_view);
497     formatTextView.setVisibility(View.VISIBLE);
498     formatTextView.setText(getString(R.string.msg_default_format) + ": " +
499         rawResult.getBarcodeFormat().toString());
500
501     ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);
502     TextView typeTextView = (TextView) findViewById(R.id.type_text_view);
503     typeTextView.setVisibility(View.VISIBLE);
504     typeTextView.setText(getString(R.string.msg_default_type) + ": " +
505         resultHandler.getType().toString());
506
507     DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
508     String formattedTime = formatter.format(new Date(rawResult.getTimestamp()));
509     TextView timeTextView = (TextView) findViewById(R.id.time_text_view);
510     timeTextView.setVisibility(View.VISIBLE);
511     timeTextView.setText(getString(R.string.msg_default_time) + ": " + formattedTime);
512
513     TextView contentsTextView = (TextView) findViewById(R.id.contents_text_view);
514     CharSequence title = getString(resultHandler.getDisplayTitle());
515     SpannableStringBuilder styled = new SpannableStringBuilder(title + "\n\n");
516     styled.setSpan(new UnderlineSpan(), 0, title.length(), 0);
517     CharSequence displayContents = resultHandler.getDisplayContents();
518     styled.append(displayContents);
519     contentsTextView.setText(styled);
520
521     int buttonCount = resultHandler.getButtonCount();
522     ViewGroup buttonView = (ViewGroup) findViewById(R.id.result_button_view);
523     buttonView.requestFocus();
524     for (int x = 0; x < ResultHandler.MAX_BUTTON_COUNT; x++) {
525       TextView button = (TextView) buttonView.getChildAt(x);
526       if (x < buttonCount) {
527         button.setVisibility(View.VISIBLE);
528         button.setText(resultHandler.getButtonText(x));
529         button.setOnClickListener(new ResultButtonListener(resultHandler, x));
530       } else {
531         button.setVisibility(View.GONE);
532       }
533     }
534
535     if (copyToClipboard) {
536       ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
537       clipboard.setText(displayContents);
538     }
539   }
540
541   // Briefly show the contents of the barcode, then handle the result outside Barcode Scanner.
542   private void handleDecodeExternally(Result rawResult, Bitmap barcode) {
543     viewfinderView.drawResultBitmap(barcode);
544
545     // Since this message will only be shown for a second, just tell the user what kind of
546     // barcode was found (e.g. contact info) rather than the full contents, which they won't
547     // have time to read.
548     ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);
549     TextView textView = (TextView) findViewById(R.id.status_text_view);
550     textView.setGravity(Gravity.CENTER);
551     textView.setTextSize(18.0f);
552     textView.setText(getString(resultHandler.getDisplayTitle()));
553
554     statusView.setBackgroundColor(getResources().getColor(R.color.transparent));
555
556     if (copyToClipboard) {
557       ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
558       clipboard.setText(resultHandler.getDisplayContents());
559     }
560
561     if (source == Source.NATIVE_APP_INTENT) {
562       // Hand back whatever action they requested - this can be changed to Intents.Scan.ACTION when
563       // the deprecated intent is retired.
564       Intent intent = new Intent(getIntent().getAction());
565       intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
566       intent.putExtra(Intents.Scan.RESULT, rawResult.toString());
567       intent.putExtra(Intents.Scan.RESULT_FORMAT, rawResult.getBarcodeFormat().toString());
568       Message message = Message.obtain(handler, R.id.return_scan_result);
569       message.obj = intent;
570       handler.sendMessageDelayed(message, INTENT_RESULT_DURATION);
571     } else if (source == Source.PRODUCT_SEARCH_LINK) {
572       // Reformulate the URL which triggered us into a query, so that the request goes to the same
573       // TLD as the scan URL.
574       Message message = Message.obtain(handler, R.id.launch_product_query);
575       int end = sourceUrl.lastIndexOf("/scan");
576       message.obj = sourceUrl.substring(0, end) + "?q=" +
577           resultHandler.getDisplayContents().toString() + "&source=zxing";
578       handler.sendMessageDelayed(message, INTENT_RESULT_DURATION);
579     } else if (source == Source.ZXING_LINK) {
580         // Replace each occurrence of RETURN_CODE_PLACEHOLDER in the returnUrlTemplate
581         // with the scanned code. This allows both queries and REST-style URLs to work.
582       Message message = Message.obtain(handler, R.id.launch_product_query);
583       message.obj = returnUrlTemplate.replace(RETURN_CODE_PLACEHOLDER, resultHandler.getDisplayContents().toString());
584       handler.sendMessageDelayed(message, INTENT_RESULT_DURATION);
585     }
586   }
587
588   /**
589    * We want the help screen to be shown automatically the first time a new version of the app is
590    * run. The easiest way to do this is to check android:versionCode from the manifest, and compare
591    * it to a value stored as a preference.
592    */
593   private boolean showHelpOnFirstLaunch() {
594     try {
595       PackageInfo info = getPackageManager().getPackageInfo(PACKAGE_NAME, 0);
596       int currentVersion = info.versionCode;
597       // Since we're paying to talk to the PackageManager anyway, it makes sense to cache the app
598       // version name here for display in the about box later.
599       this.versionName = info.versionName;
600       SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
601       int lastVersion = prefs.getInt(PreferencesActivity.KEY_HELP_VERSION_SHOWN, 0);
602       if (currentVersion > lastVersion) {
603         prefs.edit().putInt(PreferencesActivity.KEY_HELP_VERSION_SHOWN, currentVersion).commit();
604         Intent intent = new Intent(this, HelpActivity.class);
605         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
606         // Show the default page on a clean install, and the what's new page on an upgrade.
607         String page = (lastVersion == 0) ? HelpActivity.DEFAULT_PAGE : HelpActivity.WHATS_NEW_PAGE;
608         intent.putExtra(HelpActivity.REQUESTED_PAGE_KEY, page);
609         startActivity(intent);
610         return true;
611       }
612     } catch (PackageManager.NameNotFoundException e) {
613       Log.w(TAG, e);
614     }
615     return false;
616   }
617
618   /**
619    * Creates the beep MediaPlayer in advance so that the sound can be triggered with the least
620    * latency possible.
621    */
622   private void initBeepSound() {
623     if (playBeep && mediaPlayer == null) {
624       // The volume on STREAM_SYSTEM is not adjustable, and users found it too loud,
625       // so we now play on the music stream.
626       setVolumeControlStream(AudioManager.STREAM_MUSIC);
627       mediaPlayer = new MediaPlayer();
628       mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
629       mediaPlayer.setOnCompletionListener(beepListener);
630
631       AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.beep);
632       try {
633         mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(),
634             file.getLength());
635         file.close();
636         mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
637         mediaPlayer.prepare();
638       } catch (IOException e) {
639         mediaPlayer = null;
640       }
641     }
642   }
643
644   private void playBeepSoundAndVibrate() {
645     if (playBeep && mediaPlayer != null) {
646       mediaPlayer.start();
647     }
648     if (vibrate) {
649       Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
650       vibrator.vibrate(VIBRATE_DURATION);
651     }
652   }
653
654   private void initCamera(SurfaceHolder surfaceHolder) {
655     try {
656       CameraManager.get().openDriver(surfaceHolder);
657     } catch (IOException ioe) {
658       Log.w(TAG, ioe);
659       displayFrameworkBugMessageAndExit();
660       return;
661     } catch (RuntimeException e) {
662       // Barcode Scanner has seen crashes in the wild of this variety:
663       // java.?lang.?RuntimeException: Fail to connect to camera service
664       Log.e(TAG, e.toString());
665       displayFrameworkBugMessageAndExit();
666       return;
667     }
668     if (handler == null) {
669       boolean beginScanning = lastResult == null;
670       handler = new CaptureActivityHandler(this, decodeFormats, characterSet, beginScanning);
671     }
672   }
673
674   private void displayFrameworkBugMessageAndExit() {
675     AlertDialog.Builder builder = new AlertDialog.Builder(this);
676     builder.setTitle(getString(R.string.app_name));
677     builder.setMessage(getString(R.string.msg_camera_framework_bug));
678     builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
679       public void onClick(DialogInterface dialogInterface, int i) {
680         finish();
681       }
682     });
683     builder.show();
684   }
685
686   private void resetStatusView() {
687     resultView.setVisibility(View.GONE);
688     statusView.setVisibility(View.VISIBLE);
689     statusView.setBackgroundColor(getResources().getColor(R.color.status_view));
690     viewfinderView.setVisibility(View.VISIBLE);
691
692     TextView textView = (TextView) findViewById(R.id.status_text_view);
693     textView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
694     textView.setTextSize(14.0f);
695     textView.setText(R.string.msg_default_status);
696     lastResult = null;
697   }
698
699   public void drawViewfinder() {
700     viewfinderView.drawViewfinder();
701   }
702
703   /**
704    * When the beep has finished playing, rewind to queue up another one.
705    */
706   private static class BeepListener implements OnCompletionListener {
707     public void onCompletion(MediaPlayer mediaPlayer) {
708       mediaPlayer.seekTo(0);
709     }
710   }
711 }