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