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