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