92c89850dc2d5ceb3f424433ee8d3087c4599a27
[public/netxms.git] / android / src / console / src / org / netxms / ui / android / tools / BarcodeScannerIntegrator.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 org.netxms.ui.android.tools;
18
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24
25 import android.app.Activity;
26 import android.app.AlertDialog;
27 import android.content.ActivityNotFoundException;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.net.Uri;
33 import android.util.Log;
34
35 /**
36 * <p>
37 * A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple way to invoke barcode
38 * scanning and receive the result, without any need to integrate, modify, or learn the project's source code.
39 * </p>
40 *
41 * <h2>Initiating a barcode scan</h2>
42 *
43 * <p>
44 * To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait for the result in your
45 * app.
46 * </p>
47 *
48 * <p>
49 * It does require that the Barcode Scanner (or work-alike) application is installed. The {@link #initiateScan()} method will prompt
50 * the user to download the application, if needed.
51 * </p>
52 *
53 * <p>
54 * There are a few steps to using this integration. First, your {@link Activity} must implement the method
55 * {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:
56 * </p>
57 *
58 * <pre>
59 * {@code
60 * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
61 * IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
62 * if (scanResult != null) {
63 * // handle scan result
64 * }
65 * // else continue with any other code you need in the method
66 * ...
67 * }
68 * }
69 * </pre>
70 *
71 * <p>
72 * This is where you will handle a scan result.
73 * </p>
74 *
75 * <p>
76 * Second, just call this in response to a user action somewhere to begin the scan process:
77 * </p>
78 *
79 * <pre>
80 * {
81 * &#064;code
82 * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
83 * integrator.initiateScan();
84 * }
85 * </pre>
86 *
87 * <p>
88 * Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the user was prompted to download the
89 * application. This lets the calling app potentially manage the dialog. In particular, ideally, the app dismisses the dialog if
90 * it's still active in its {@link Activity#onPause()} method.
91 * </p>
92 *
93 * <p>
94 * You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use {@link #setTitleByID(int)}
95 * to set the title by string resource ID.) Likewise, the prompt message, and yes/no button labels can be changed.
96 * </p>
97 *
98 * <p>
99 * By default, this will only allow applications that are known to respond to this intent correctly do so. The apps that are allowed
100 * to response can be set with {@link #setTargetApplications(Collection)}. For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY}
101 * to only target the Barcode Scanner app itself.
102 * </p>
103 *
104 * <h2>Sharing text via barcode</h2>
105 *
106 * <p>
107 * To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.
108 * </p>
109 *
110 * <p>
111 * Some code, particularly download integration, was contributed from the Anobiit application.
112 * </p>
113 *
114 * @author Sean Owen
115 * @author Fred Lin
116 * @author Isaac Potoczny-Jones
117 * @author Brad Drehmer
118 * @author gcstang
119 */
120 public class BarcodeScannerIntegrator
121 {
122
123 public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
124 private static final String TAG = BarcodeScannerIntegrator.class.getSimpleName();
125
126 public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
127 public static final String DEFAULT_MESSAGE = "This application requires Barcode Scanner. Would you like to install it?";
128 public static final String DEFAULT_YES = "Yes";
129 public static final String DEFAULT_NO = "No";
130
131 private static final String BS_PACKAGE = "com.google.zxing.client.android";
132
133 // supported barcode formats
134 public static final Collection<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
135 public static final Collection<String> ONE_D_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93",
136 "CODE_128", "ITF", "RSS_14", "RSS_EXPANDED");
137 public static final Collection<String> QR_CODE_TYPES = Collections.singleton("QR_CODE");
138 public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
139
140 public static final Collection<String> ALL_CODE_TYPES = null;
141
142 public static final Collection<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singleton(BS_PACKAGE);
143 public static final Collection<String> TARGET_ALL_KNOWN = list(BS_PACKAGE, // Barcode Scanner
144 "com.srowen.bs.android", // Barcode Scanner+
145 "com.srowen.bs.android.simple" // Barcode Scanner+ Simple
146 // TODO add more -- what else supports this intent?
147 );
148
149 private final Activity activity;
150 private String title;
151 private String message;
152 private String buttonYes;
153 private String buttonNo;
154 private Collection<String> targetApplications;
155
156 public BarcodeScannerIntegrator(Activity activity)
157 {
158 this.activity = activity;
159 title = DEFAULT_TITLE;
160 message = DEFAULT_MESSAGE;
161 buttonYes = DEFAULT_YES;
162 buttonNo = DEFAULT_NO;
163 targetApplications = TARGET_ALL_KNOWN;
164 }
165
166 public String getTitle()
167 {
168 return title;
169 }
170
171 public void setTitle(String title)
172 {
173 this.title = title;
174 }
175
176 public void setTitleByID(int titleID)
177 {
178 title = activity.getString(titleID);
179 }
180
181 public String getMessage()
182 {
183 return message;
184 }
185
186 public void setMessage(String message)
187 {
188 this.message = message;
189 }
190
191 public void setMessageByID(int messageID)
192 {
193 message = activity.getString(messageID);
194 }
195
196 public String getButtonYes()
197 {
198 return buttonYes;
199 }
200
201 public void setButtonYes(String buttonYes)
202 {
203 this.buttonYes = buttonYes;
204 }
205
206 public void setButtonYesByID(int buttonYesID)
207 {
208 buttonYes = activity.getString(buttonYesID);
209 }
210
211 public String getButtonNo()
212 {
213 return buttonNo;
214 }
215
216 public void setButtonNo(String buttonNo)
217 {
218 this.buttonNo = buttonNo;
219 }
220
221 public void setButtonNoByID(int buttonNoID)
222 {
223 buttonNo = activity.getString(buttonNoID);
224 }
225
226 public Collection<String> getTargetApplications()
227 {
228 return targetApplications;
229 }
230
231 public void setTargetApplications(Collection<String> targetApplications)
232 {
233 this.targetApplications = targetApplications;
234 }
235
236 public void setSingleTargetApplication(String targetApplication)
237 {
238 this.targetApplications = Collections.singleton(targetApplication);
239 }
240
241 /**
242 * Initiates a scan for all known barcode types.
243 */
244 public AlertDialog initiateScan()
245 {
246 List<String> formats = new ArrayList<String>();
247 formats.addAll(PRODUCT_CODE_TYPES);
248 formats.addAll(ONE_D_CODE_TYPES);
249 formats.addAll(QR_CODE_TYPES);
250 formats.addAll(DATA_MATRIX_TYPES);
251 return initiateScan(formats);
252 }
253
254 /**
255 * Initiates a scan only for a certain set of barcode types, given as strings corresponding to their names in ZXing's
256 * {@code BarcodeFormat} class like "UPC_A". You can supply constants like {@link #PRODUCT_CODE_TYPES} for example.
257 */
258 public AlertDialog initiateScan(Collection<String> desiredBarcodeFormats)
259 {
260 Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
261 intentScan.addCategory(Intent.CATEGORY_DEFAULT);
262
263 // check which types of codes to scan for
264 if (desiredBarcodeFormats != null)
265 {
266 // set the desired barcode types
267 StringBuilder joinedByComma = new StringBuilder();
268 for (String format : desiredBarcodeFormats)
269 {
270 if (joinedByComma.length() > 0)
271 {
272 joinedByComma.append(',');
273 }
274 joinedByComma.append(format);
275 }
276 intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
277 }
278
279 String targetAppPackage = findTargetAppPackage(intentScan);
280 if (targetAppPackage == null)
281 {
282 return showDownloadDialog();
283 }
284 intentScan.setPackage(targetAppPackage);
285 intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
286 intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
287 intentScan.putExtra("SCAN_MODE", "PRODUCT_MODE");
288 intentScan.putExtra("RESULT_DISPLAY_DURATION_MS", 1000L);
289 startActivityForResult(intentScan, REQUEST_CODE);
290 return null;
291 }
292
293 /**
294 * Start an activity.<br>
295 * This method is defined to allow different methods of activity starting for newer versions of Android and for compatibility
296 * library.
297 *
298 * @param intent Intent to start.
299 * @param code Request code for the activity
300 * @see android.app.Activity#startActivityForResult(Intent, int)
301 * @see android.app.Fragment#startActivityForResult(Intent, int)
302 */
303 protected void startActivityForResult(Intent intent, int code)
304 {
305 activity.startActivityForResult(intent, code);
306 }
307
308 private String findTargetAppPackage(Intent intent)
309 {
310 PackageManager pm = activity.getPackageManager();
311 List<ResolveInfo> availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
312 if (availableApps != null)
313 {
314 for (ResolveInfo availableApp : availableApps)
315 {
316 String packageName = availableApp.activityInfo.packageName;
317 if (targetApplications.contains(packageName))
318 {
319 return packageName;
320 }
321 }
322 }
323 return null;
324 }
325
326 private AlertDialog showDownloadDialog()
327 {
328 AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
329 downloadDialog.setTitle(title);
330 downloadDialog.setMessage(message);
331 downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener()
332 {
333 @Override
334 public void onClick(DialogInterface dialogInterface, int i)
335 {
336 Uri uri = Uri.parse("market://details?id=" + BS_PACKAGE);
337 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
338 try
339 {
340 activity.startActivity(intent);
341 }
342 catch (ActivityNotFoundException anfe)
343 {
344 // Hmm, market is not installed
345 Log.e(TAG, "Android Market is not installed; cannot install Barcode Scanner");
346 }
347 }
348 });
349 downloadDialog.setNegativeButton(buttonNo, new DialogInterface.OnClickListener()
350 {
351 @Override
352 public void onClick(DialogInterface dialogInterface, int i)
353 {
354 }
355 });
356 return downloadDialog.show();
357 }
358
359 /**
360 * <p>
361 * Call this from your {@link Activity}'s {@link Activity#onActivityResult(int, int, Intent)} method.
362 * </p>
363 *
364 * @return null if the event handled here was not related to this class, or else an {@link BarcodeScannerIntentResult} containing
365 * the result of the scan. If the user cancelled scanning, the fields will be null.
366 */
367 public static BarcodeScannerIntentResult parseActivityResult(int requestCode, int resultCode, Intent intent)
368 {
369 if (requestCode == REQUEST_CODE)
370 {
371 if (resultCode == Activity.RESULT_OK)
372 {
373 String contents = intent.getStringExtra("SCAN_RESULT");
374 String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
375 byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
376 int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
377 Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
378 String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
379 return new BarcodeScannerIntentResult(contents, formatName, rawBytes, orientation, errorCorrectionLevel);
380 }
381 return new BarcodeScannerIntentResult();
382 }
383 return null;
384 }
385
386 /**
387 * Shares the given text by encoding it as a barcode, such that another user can scan the text off the screen of the device.
388 *
389 * @param text the text string to encode as a barcode
390 */
391 public void shareText(CharSequence text)
392 {
393 Intent intent = new Intent();
394 intent.addCategory(Intent.CATEGORY_DEFAULT);
395 intent.setAction(BS_PACKAGE + ".ENCODE");
396 intent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
397 intent.putExtra("ENCODE_DATA", text);
398 String targetAppPackage = findTargetAppPackage(intent);
399 if (targetAppPackage == null)
400 {
401 showDownloadDialog();
402 }
403 else
404 {
405 intent.setPackage(targetAppPackage);
406 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
407 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
408 activity.startActivity(intent);
409 }
410 }
411
412 private static Collection<String> list(String... values)
413 {
414 return Collections.unmodifiableCollection(Arrays.asList(values));
415 }
416
417 }