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