]> rtime.felk.cvut.cz Git - hornmich/skoda-qr-demo.git/blob - QRScanner/mobile/src/main/java/cz/cvut/fel/dce/qrscanner/integration/IntentIntegrator.java
Initial commit
[hornmich/skoda-qr-demo.git] / QRScanner / mobile / src / main / java / cz / cvut / fel / dce / qrscanner / integration / IntentIntegrator.java
1 /*
2  * Copyright 2009 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 cz.cvut.fel.dce.qrscanner.integration;
18
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;
30
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;
36 import java.util.Map;
37
38 /**
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>
42  *
43  * <h2>Initiating a barcode scan</h2>
44  *
45  * <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
46  * for the result in your app.</p>
47  *
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>
50  *
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>
53  *
54  * <pre>{@code
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
59  *   }
60  *   // else continue with any other code you need in the method
61  *   ...
62  * }
63  * }</pre>
64  *
65  * <p>This is where you will handle a scan result.</p>
66  *
67  * <p>Second, just call this in response to a user action somewhere to begin the scan process:</p>
68  *
69  * <pre>{@code
70  * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
71  * integrator.initiateScan();
72  * }</pre>
73  *
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()}
77  * method.</p>
78  * 
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>
82  *
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
85  * simplified API.</p>
86  * 
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>
90  *
91  * <h2>Sharing text via barcode</h2>
92  *
93  * <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p>
94  *
95  * <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
96  *
97  * <h2>Enabling experimental barcode formats</h2>
98  *
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
102  * formats.</p>
103  *
104  * @author Sean Owen
105  * @author Fred Lin
106  * @author Isaac Potoczny-Jones
107  * @author Brad Drehmer
108  * @author gcstang
109  */
110 public class IntentIntegrator {
111
112   public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
113   private static final String TAG = IntentIntegrator.class.getSimpleName();
114
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";
120
121   private static final String BS_PACKAGE = "cz.cvut.fel.dce.barcodescanner";
122   private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
123
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");
131
132   public static final Collection<String> ALL_CODE_TYPES = null;
133   
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?
140       );
141   
142   private final Activity activity;
143   private final Fragment fragment;
144
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);
151
152   /**
153    * @param activity {@link android.app.Activity} invoking the integration
154    */
155   public IntentIntegrator(Activity activity) {
156     this.activity = activity;
157     this.fragment = null;
158     initializeConfiguration();
159   }
160
161   /**
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}
165    */
166   public IntentIntegrator(Fragment fragment) {
167     this.activity = fragment.getActivity();
168     this.fragment = fragment;
169     initializeConfiguration();
170   }
171
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;
178   }
179   
180   public String getTitle() {
181     return title;
182   }
183   
184   public void setTitle(String title) {
185     this.title = title;
186   }
187
188   public void setTitleByID(int titleID) {
189     title = activity.getString(titleID);
190   }
191
192   public String getMessage() {
193     return message;
194   }
195
196   public void setMessage(String message) {
197     this.message = message;
198   }
199
200   public void setMessageByID(int messageID) {
201     message = activity.getString(messageID);
202   }
203
204   public String getButtonYes() {
205     return buttonYes;
206   }
207
208   public void setButtonYes(String buttonYes) {
209     this.buttonYes = buttonYes;
210   }
211
212   public void setButtonYesByID(int buttonYesID) {
213     buttonYes = activity.getString(buttonYesID);
214   }
215
216   public String getButtonNo() {
217     return buttonNo;
218   }
219
220   public void setButtonNo(String buttonNo) {
221     this.buttonNo = buttonNo;
222   }
223
224   public void setButtonNoByID(int buttonNoID) {
225     buttonNo = activity.getString(buttonNoID);
226   }
227   
228   public Collection<String> getTargetApplications() {
229     return targetApplications;
230   }
231   
232   public final void setTargetApplications(List<String> targetApplications) {
233     if (targetApplications.isEmpty()) {
234       throw new IllegalArgumentException("No target applications");
235     }
236     this.targetApplications = targetApplications;
237   }
238   
239   public void setSingleTargetApplication(String targetApplication) {
240     this.targetApplications = Collections.singletonList(targetApplication);
241   }
242
243   public Map<String,?> getMoreExtras() {
244     return moreExtras;
245   }
246
247   public final void addExtra(String key, Object value) {
248     moreExtras.put(key, value);
249   }
250
251   /**
252    * Initiates a scan for all known barcode types with the default camera.
253    *
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.
256    */
257   public final AlertDialog initiateScan() {
258     return initiateScan(ALL_CODE_TYPES, -1);
259   }
260   
261   /**
262    * Initiates a scan for all known barcode types with the specified camera.
263    *
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.
267    */
268   public final AlertDialog initiateScan(int cameraId) {
269     return initiateScan(ALL_CODE_TYPES, cameraId);
270   }
271
272   /**
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.
276    *
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.
280    */
281   public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats) {
282     return initiateScan(desiredBarcodeFormats, -1);
283   }
284   
285   /**
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.
289    *
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
294    */
295   public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats, int cameraId) {
296     Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
297     intentScan.addCategory(Intent.CATEGORY_DEFAULT);
298
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(',');
306         }
307         joinedByComma.append(format);
308       }
309       intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
310     }
311
312     // check requested camera ID
313     if (cameraId >= 0) {
314       intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
315     }
316
317     String targetAppPackage = findTargetAppPackage(intentScan);
318     if (targetAppPackage == null) {
319       return showDownloadDialog();
320     }
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);
326     return null;
327   }
328
329   /**
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.
332    *
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)
337    */
338   protected void startActivityForResult(Intent intent, int code) {
339     if (fragment == null) {
340       activity.startActivityForResult(intent, code);
341     } else {
342       fragment.startActivityForResult(intent, code);
343     }
344   }
345   
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)) {
352           return targetApp;
353         }
354       }
355     }
356     return null;
357   }
358   
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)) {
363         return true;
364       }
365     }
366     return false;
367   }
368
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() {
374       @Override
375       public void onClick(DialogInterface dialogInterface, int i) {
376         String packageName;
377         if (targetApplications.contains(BS_PACKAGE)) {
378           // Prefer to suggest download of BS if it's anywhere in the list
379           packageName = BS_PACKAGE;
380         } else {
381           // Otherwise, first option:
382           packageName = targetApplications.get(0);
383         }
384         Uri uri = Uri.parse("market://details?id=" + packageName);
385         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
386         try {
387           if (fragment == null) {
388             activity.startActivity(intent);
389           } else {
390             fragment.startActivity(intent);
391           }
392         } catch (ActivityNotFoundException anfe) {
393           // Hmm, market is not installed
394           Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
395         }
396       }
397     });
398     downloadDialog.setNegativeButton(buttonNo, null);
399     downloadDialog.setCancelable(true);
400     return downloadDialog.show();
401   }
402
403
404   /**
405    * <p>Call this from your {@link android.app.Activity}'s
406    * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} method.</p>
407    *
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.
414    */
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,
425                                 formatName,
426                                 rawBytes,
427                                 orientation,
428                                 errorCorrectionLevel);
429       }
430       return new IntentResult();
431     }
432     return null;
433   }
434
435
436   /**
437    * Defaults to type "TEXT_TYPE".
438    *
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)
443    */
444   public final AlertDialog shareText(CharSequence text) {
445     return shareText(text, "TEXT_TYPE");
446   }
447
448   /**
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.
451    *
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
456    */
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();
466     }
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);
473     } else {
474       fragment.startActivity(intent);
475     }
476     return null;
477   }
478   
479   private static List<String> list(String... values) {
480     return Collections.unmodifiableList(Arrays.asList(values));
481   }
482
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();
487       // Kind of hacky
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);
500       } else {
501         intent.putExtra(key, value.toString());
502       }
503     }
504   }
505
506 }