2 * Copyright 2009 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package cz.cvut.fel.dce.qrscanner.integration;
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Fragment;
22 import android.content.ActivityNotFoundException;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.util.Log;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.List;
39 * <p>A utility class which helps ease integration with Barcode Scanner via {@link android.content.Intent}s. This is a simple
40 * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
41 * project's source code.</p>
43 * <h2>Initiating a barcode scan</h2>
45 * <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
46 * for the result in your app.</p>
48 * <p>It does require that the Barcode Scanner (or work-alike) application is installed. The
49 * {@link #initiateScan()} method will prompt the user to download the application, if needed.</p>
51 * <p>There are a few steps to using this integration. First, your {@link android.app.Activity} must implement
52 * the method {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} and include a line of code like this:</p>
55 * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
56 * IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
57 * if (scanResult != null) {
58 * // handle scan result
60 * // else continue with any other code you need in the method
65 * <p>This is where you will handle a scan result.</p>
67 * <p>Second, just call this in response to a user action somewhere to begin the scan process:</p>
70 * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
71 * integrator.initiateScan();
74 * <p>Note that {@link #initiateScan()} returns an {@link android.app.AlertDialog} which is non-null if the
75 * user was prompted to download the application. This lets the calling app potentially manage the dialog.
76 * In particular, ideally, the app dismisses the dialog if it's still active in its {@link android.app.Activity#onPause()}
79 * <p>You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use
80 * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and
81 * yes/no button labels can be changed.</p>
83 * <p>Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used
84 * to invoke the scanner. This can be used to set additional options not directly exposed by this
87 * <p>By default, this will only allow applications that are known to respond to this intent correctly
88 * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(java.util.List)}.
89 * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.</p>
91 * <h2>Sharing text via barcode</h2>
93 * <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p>
95 * <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
97 * <h2>Enabling experimental barcode formats</h2>
99 * <p>Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as
100 * PDF417. Use {@link #initiateScan(java.util.Collection)} with
101 * a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such
106 * @author Isaac Potoczny-Jones
107 * @author Brad Drehmer
110 public class IntentIntegrator {
112 public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
113 private static final String TAG = IntentIntegrator.class.getSimpleName();
115 public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
116 public static final String DEFAULT_MESSAGE =
117 "This application requires Barcode Scanner. Would you like to install it?";
118 public static final String DEFAULT_YES = "Yes";
119 public static final String DEFAULT_NO = "No";
121 private static final String BS_PACKAGE = "cz.cvut.fel.dce.barcodescanner";
122 private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
124 // supported barcode formats
125 public static final Collection<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
126 public static final Collection<String> ONE_D_CODE_TYPES =
127 list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128",
128 "ITF", "RSS_14", "RSS_EXPANDED");
129 public static final Collection<String> QR_CODE_TYPES = Collections.singleton("QR_CODE");
130 public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
132 public static final Collection<String> ALL_CODE_TYPES = null;
134 public static final List<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
135 public static final List<String> TARGET_ALL_KNOWN = list(
136 BSPLUS_PACKAGE, // Barcode Scanner+
137 BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple
138 BS_PACKAGE // Barcode Scanner
139 // What else supports this intent?
142 private final Activity activity;
143 private final Fragment fragment;
145 private String title;
146 private String message;
147 private String buttonYes;
148 private String buttonNo;
149 private List<String> targetApplications;
150 private final Map<String,Object> moreExtras = new HashMap<String,Object>(3);
153 * @param activity {@link android.app.Activity} invoking the integration
155 public IntentIntegrator(Activity activity) {
156 this.activity = activity;
157 this.fragment = null;
158 initializeConfiguration();
162 * @param fragment {@link android.app.Fragment} invoking the integration.
163 * {@link #startActivityForResult(android.content.Intent, int)} will be called on the {@link android.app.Fragment} instead
164 * of an {@link android.app.Activity}
166 public IntentIntegrator(Fragment fragment) {
167 this.activity = fragment.getActivity();
168 this.fragment = fragment;
169 initializeConfiguration();
172 private void initializeConfiguration() {
173 title = DEFAULT_TITLE;
174 message = DEFAULT_MESSAGE;
175 buttonYes = DEFAULT_YES;
176 buttonNo = DEFAULT_NO;
177 targetApplications = TARGET_ALL_KNOWN;
180 public String getTitle() {
184 public void setTitle(String title) {
188 public void setTitleByID(int titleID) {
189 title = activity.getString(titleID);
192 public String getMessage() {
196 public void setMessage(String message) {
197 this.message = message;
200 public void setMessageByID(int messageID) {
201 message = activity.getString(messageID);
204 public String getButtonYes() {
208 public void setButtonYes(String buttonYes) {
209 this.buttonYes = buttonYes;
212 public void setButtonYesByID(int buttonYesID) {
213 buttonYes = activity.getString(buttonYesID);
216 public String getButtonNo() {
220 public void setButtonNo(String buttonNo) {
221 this.buttonNo = buttonNo;
224 public void setButtonNoByID(int buttonNoID) {
225 buttonNo = activity.getString(buttonNoID);
228 public Collection<String> getTargetApplications() {
229 return targetApplications;
232 public final void setTargetApplications(List<String> targetApplications) {
233 if (targetApplications.isEmpty()) {
234 throw new IllegalArgumentException("No target applications");
236 this.targetApplications = targetApplications;
239 public void setSingleTargetApplication(String targetApplication) {
240 this.targetApplications = Collections.singletonList(targetApplication);
243 public Map<String,?> getMoreExtras() {
247 public final void addExtra(String key, Object value) {
248 moreExtras.put(key, value);
252 * Initiates a scan for all known barcode types with the default camera.
254 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
255 * if a prompt was needed, or null otherwise.
257 public final AlertDialog initiateScan() {
258 return initiateScan(ALL_CODE_TYPES, -1);
262 * Initiates a scan for all known barcode types with the specified camera.
264 * @param cameraId camera ID of the camera to use. A negative value means "no preference".
265 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
266 * if a prompt was needed, or null otherwise.
268 public final AlertDialog initiateScan(int cameraId) {
269 return initiateScan(ALL_CODE_TYPES, cameraId);
273 * Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding
274 * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
275 * like {@link #PRODUCT_CODE_TYPES} for example.
277 * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
278 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
279 * if a prompt was needed, or null otherwise.
281 public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats) {
282 return initiateScan(desiredBarcodeFormats, -1);
286 * Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding
287 * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
288 * like {@link #PRODUCT_CODE_TYPES} for example.
290 * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
291 * @param cameraId camera ID of the camera to use. A negative value means "no preference".
292 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
293 * if a prompt was needed, or null otherwise
295 public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats, int cameraId) {
296 Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
297 intentScan.addCategory(Intent.CATEGORY_DEFAULT);
299 // check which types of codes to scan for
300 if (desiredBarcodeFormats != null) {
301 // set the desired barcode types
302 StringBuilder joinedByComma = new StringBuilder();
303 for (String format : desiredBarcodeFormats) {
304 if (joinedByComma.length() > 0) {
305 joinedByComma.append(',');
307 joinedByComma.append(format);
309 intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
312 // check requested camera ID
314 intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
317 String targetAppPackage = findTargetAppPackage(intentScan);
318 if (targetAppPackage == null) {
319 return showDownloadDialog();
321 intentScan.setPackage(targetAppPackage);
322 intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
323 intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
324 attachMoreExtras(intentScan);
325 startActivityForResult(intentScan, REQUEST_CODE);
330 * Start an activity. This method is defined to allow different methods of activity starting for
331 * newer versions of Android and for compatibility library.
333 * @param intent Intent to start.
334 * @param code Request code for the activity
335 * @see android.app.Activity#startActivityForResult(android.content.Intent, int)
336 * @see android.app.Fragment#startActivityForResult(android.content.Intent, int)
338 protected void startActivityForResult(Intent intent, int code) {
339 if (fragment == null) {
340 activity.startActivityForResult(intent, code);
342 fragment.startActivityForResult(intent, code);
346 private String findTargetAppPackage(Intent intent) {
347 PackageManager pm = activity.getPackageManager();
348 List<ResolveInfo> availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
349 if (availableApps != null) {
350 for (String targetApp : targetApplications) {
351 if (contains(availableApps, targetApp)) {
359 private static boolean contains(Iterable<ResolveInfo> availableApps, String targetApp) {
360 for (ResolveInfo availableApp : availableApps) {
361 String packageName = availableApp.activityInfo.packageName;
362 if (targetApp.equals(packageName)) {
369 private AlertDialog showDownloadDialog() {
370 AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
371 downloadDialog.setTitle(title);
372 downloadDialog.setMessage(message);
373 downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() {
375 public void onClick(DialogInterface dialogInterface, int i) {
377 if (targetApplications.contains(BS_PACKAGE)) {
378 // Prefer to suggest download of BS if it's anywhere in the list
379 packageName = BS_PACKAGE;
381 // Otherwise, first option:
382 packageName = targetApplications.get(0);
384 Uri uri = Uri.parse("market://details?id=" + packageName);
385 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
387 if (fragment == null) {
388 activity.startActivity(intent);
390 fragment.startActivity(intent);
392 } catch (ActivityNotFoundException anfe) {
393 // Hmm, market is not installed
394 Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
398 downloadDialog.setNegativeButton(buttonNo, null);
399 downloadDialog.setCancelable(true);
400 return downloadDialog.show();
405 * <p>Call this from your {@link android.app.Activity}'s
406 * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} method.</p>
408 * @param requestCode request code from {@code onActivityResult()}
409 * @param resultCode result code from {@code onActivityResult()}
410 * @param intent {@link android.content.Intent} from {@code onActivityResult()}
411 * @return null if the event handled here was not related to this class, or
412 * else an {@link com.google.zxing.integration.android.IntentResult} containing the result of the scan. If the user cancelled scanning,
413 * the fields will be null.
415 public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
416 if (requestCode == REQUEST_CODE) {
417 if (resultCode == Activity.RESULT_OK) {
418 String contents = intent.getStringExtra("SCAN_RESULT");
419 String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
420 byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
421 int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
422 Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
423 String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
424 return new IntentResult(contents,
428 errorCorrectionLevel);
430 return new IntentResult();
437 * Defaults to type "TEXT_TYPE".
439 * @param text the text string to encode as a barcode
440 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
441 * if a prompt was needed, or null otherwise
442 * @see #shareText(CharSequence, CharSequence)
444 public final AlertDialog shareText(CharSequence text) {
445 return shareText(text, "TEXT_TYPE");
449 * Shares the given text by encoding it as a barcode, such that another user can
450 * scan the text off the screen of the device.
452 * @param text the text string to encode as a barcode
453 * @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants.
454 * @return the {@link android.app.AlertDialog} that was shown to the user prompting them to download the app
455 * if a prompt was needed, or null otherwise
457 public final AlertDialog shareText(CharSequence text, CharSequence type) {
458 Intent intent = new Intent();
459 intent.addCategory(Intent.CATEGORY_DEFAULT);
460 intent.setAction(BS_PACKAGE + ".ENCODE");
461 intent.putExtra("ENCODE_TYPE", type);
462 intent.putExtra("ENCODE_DATA", text);
463 String targetAppPackage = findTargetAppPackage(intent);
464 if (targetAppPackage == null) {
465 return showDownloadDialog();
467 intent.setPackage(targetAppPackage);
468 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
469 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
470 attachMoreExtras(intent);
471 if (fragment == null) {
472 activity.startActivity(intent);
474 fragment.startActivity(intent);
479 private static List<String> list(String... values) {
480 return Collections.unmodifiableList(Arrays.asList(values));
483 private void attachMoreExtras(Intent intent) {
484 for (Map.Entry<String,Object> entry : moreExtras.entrySet()) {
485 String key = entry.getKey();
486 Object value = entry.getValue();
488 if (value instanceof Integer) {
489 intent.putExtra(key, (Integer) value);
490 } else if (value instanceof Long) {
491 intent.putExtra(key, (Long) value);
492 } else if (value instanceof Boolean) {
493 intent.putExtra(key, (Boolean) value);
494 } else if (value instanceof Double) {
495 intent.putExtra(key, (Double) value);
496 } else if (value instanceof Float) {
497 intent.putExtra(key, (Float) value);
498 } else if (value instanceof Bundle) {
499 intent.putExtra(key, (Bundle) value);
501 intent.putExtra(key, value.toString());