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