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.result; 18 19 import android.telephony.PhoneNumberUtils; 20 import com.google.zxing.Result; 21 import com.google.zxing.client.android.Contents; 22 import com.google.zxing.client.android.Intents; 23 import com.google.zxing.client.android.LocaleManager; 24 import com.google.zxing.client.android.PreferencesActivity; 25 import com.google.zxing.client.android.R; 26 import com.google.zxing.client.android.book.SearchBookContentsActivity; 27 import com.google.zxing.client.result.ParsedResult; 28 import com.google.zxing.client.result.ParsedResultType; 29 import com.google.zxing.client.result.ResultParser; 30 31 import android.app.Activity; 32 import android.app.AlertDialog; 33 import android.content.ActivityNotFoundException; 34 import android.content.ContentValues; 35 import android.content.Intent; 36 import android.content.SharedPreferences; 37 import android.net.Uri; 38 import android.preference.PreferenceManager; 39 import android.provider.ContactsContract; 40 import android.util.Log; 41 42 import java.io.UnsupportedEncodingException; 43 import java.net.URLEncoder; 44 import java.util.Locale; 45 import java.util.ArrayList; 46 47 /** 48 * A base class for the Android-specific barcode handlers. These allow the app to polymorphically 49 * suggest the appropriate actions for each data type. 50 * 51 * This class also contains a bunch of utility methods to take common actions like opening a URL. 52 * They could easily be moved into a helper object, but it can't be static because the Activity 53 * instance is needed to launch an intent. 54 * 55 * @author [email protected] (Daniel Switkin) 56 * @author Sean Owen 57 */ 58 public abstract class ResultHandler { 59 60 private static final String TAG = ResultHandler.class.getSimpleName(); 61 62 private static final String[] EMAIL_TYPE_STRINGS = {"home", "work", "mobile"}; 63 private static final String[] PHONE_TYPE_STRINGS = {"home", "work", "mobile", "fax", "pager", "main"}; 64 private static final String[] ADDRESS_TYPE_STRINGS = {"home", "work"}; 65 private static final int[] EMAIL_TYPE_VALUES = { 66 ContactsContract.CommonDataKinds.Email.TYPE_HOME, 67 ContactsContract.CommonDataKinds.Email.TYPE_WORK, 68 ContactsContract.CommonDataKinds.Email.TYPE_MOBILE, 69 }; 70 private static final int[] PHONE_TYPE_VALUES = { 71 ContactsContract.CommonDataKinds.Phone.TYPE_HOME, 72 ContactsContract.CommonDataKinds.Phone.TYPE_WORK, 73 ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, 74 ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK, 75 ContactsContract.CommonDataKinds.Phone.TYPE_PAGER, 76 ContactsContract.CommonDataKinds.Phone.TYPE_MAIN, 77 }; 78 private static final int[] ADDRESS_TYPE_VALUES = { 79 ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME, 80 ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK, 81 }; 82 private static final int NO_TYPE = -1; 83 84 public static final int MAX_BUTTON_COUNT = 4; 85 86 private final ParsedResult result; 87 private final Activity activity; 88 private final Result rawResult; 89 private final String customProductSearch; 90 ResultHandler(Activity activity, ParsedResult result)91 ResultHandler(Activity activity, ParsedResult result) { 92 this(activity, result, null); 93 } 94 ResultHandler(Activity activity, ParsedResult result, Result rawResult)95 ResultHandler(Activity activity, ParsedResult result, Result rawResult) { 96 this.result = result; 97 this.activity = activity; 98 this.rawResult = rawResult; 99 this.customProductSearch = parseCustomSearchURL(); 100 } 101 getResult()102 public final ParsedResult getResult() { 103 return result; 104 } 105 hasCustomProductSearch()106 final boolean hasCustomProductSearch() { 107 return customProductSearch != null; 108 } 109 getActivity()110 final Activity getActivity() { 111 return activity; 112 } 113 114 /** 115 * Indicates how many buttons the derived class wants shown. 116 * 117 * @return The integer button count. 118 */ getButtonCount()119 public abstract int getButtonCount(); 120 121 /** 122 * The text of the nth action button. 123 * 124 * @param index From 0 to getButtonCount() - 1 125 * @return The button text as a resource ID 126 */ getButtonText(int index)127 public abstract int getButtonText(int index); 128 getDefaultButtonID()129 public Integer getDefaultButtonID() { 130 return null; 131 } 132 133 /** 134 * Execute the action which corresponds to the nth button. 135 * 136 * @param index The button that was clicked. 137 */ handleButtonPress(int index)138 public abstract void handleButtonPress(int index); 139 140 /** 141 * Some barcode contents are considered secure, and should not be saved to history, copied to 142 * the clipboard, or otherwise persisted. 143 * 144 * @return If true, do not create any permanent record of these contents. 145 */ areContentsSecure()146 public boolean areContentsSecure() { 147 return false; 148 } 149 150 /** 151 * Create a possibly styled string for the contents of the current barcode. 152 * 153 * @return The text to be displayed. 154 */ getDisplayContents()155 public CharSequence getDisplayContents() { 156 String contents = result.getDisplayResult(); 157 return contents.replace("\r", ""); 158 } 159 160 /** 161 * A string describing the kind of barcode that was found, e.g. "Found contact info". 162 * 163 * @return The resource ID of the string. 164 */ getDisplayTitle()165 public abstract int getDisplayTitle(); 166 167 /** 168 * A convenience method to get the parsed type. Should not be overridden. 169 * 170 * @return The parsed type, e.g. URI or ISBN 171 */ getType()172 public final ParsedResultType getType() { 173 return result.getType(); 174 } 175 addPhoneOnlyContact(String[] phoneNumbers,String[] phoneTypes)176 final void addPhoneOnlyContact(String[] phoneNumbers,String[] phoneTypes) { 177 addContact(null, null, null, phoneNumbers, phoneTypes, 178 null, null, null, null, null, null, null, null, null, null, null); 179 } 180 addEmailOnlyContact(String[] emails, String[] emailTypes)181 final void addEmailOnlyContact(String[] emails, String[] emailTypes) { 182 addContact(null, null, null, null, null, emails, emailTypes, null, null, null, null, null, null, null, null, null); 183 } 184 addContact(String[] names, String[] nicknames, String pronunciation, String[] phoneNumbers, String[] phoneTypes, String[] emails, String[] emailTypes, String note, String instantMessenger, String address, String addressType, String org, String title, String[] urls, String birthday, String[] geo)185 final void addContact(String[] names, 186 String[] nicknames, 187 String pronunciation, 188 String[] phoneNumbers, 189 String[] phoneTypes, 190 String[] emails, 191 String[] emailTypes, 192 String note, 193 String instantMessenger, 194 String address, 195 String addressType, 196 String org, 197 String title, 198 String[] urls, 199 String birthday, 200 String[] geo) { 201 202 // Only use the first name in the array, if present. 203 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT, ContactsContract.Contacts.CONTENT_URI); 204 intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); 205 putExtra(intent, ContactsContract.Intents.Insert.NAME, names != null && names.length > 0 ? names[0] : null); 206 207 putExtra(intent, ContactsContract.Intents.Insert.PHONETIC_NAME, pronunciation); 208 209 if (phoneNumbers != null) { 210 int phoneCount = Math.min(phoneNumbers.length, Contents.PHONE_KEYS.length); 211 for (int x = 0; x < phoneCount; x++) { 212 putExtra(intent, Contents.PHONE_KEYS[x], phoneNumbers[x]); 213 if (phoneTypes != null && x < phoneTypes.length) { 214 int type = toPhoneContractType(phoneTypes[x]); 215 if (type >= 0) { 216 intent.putExtra(Contents.PHONE_TYPE_KEYS[x], type); 217 } 218 } 219 } 220 } 221 222 if (emails != null) { 223 int emailCount = Math.min(emails.length, Contents.EMAIL_KEYS.length); 224 for (int x = 0; x < emailCount; x++) { 225 putExtra(intent, Contents.EMAIL_KEYS[x], emails[x]); 226 if (emailTypes != null && x < emailTypes.length) { 227 int type = toEmailContractType(emailTypes[x]); 228 if (type >= 0) { 229 intent.putExtra(Contents.EMAIL_TYPE_KEYS[x], type); 230 } 231 } 232 } 233 } 234 235 ArrayList<ContentValues> data = new ArrayList<>(); 236 if (urls != null) { 237 for (String url : urls) { 238 if (url != null && !url.isEmpty()) { 239 ContentValues row = new ContentValues(2); 240 row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE); 241 row.put(ContactsContract.CommonDataKinds.Website.URL, url); 242 data.add(row); 243 break; 244 } 245 } 246 } 247 248 if (birthday != null) { 249 ContentValues row = new ContentValues(3); 250 row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); 251 row.put(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY); 252 row.put(ContactsContract.CommonDataKinds.Event.START_DATE, birthday); 253 data.add(row); 254 } 255 256 if (nicknames != null) { 257 for (String nickname : nicknames) { 258 if (nickname != null && !nickname.isEmpty()) { 259 ContentValues row = new ContentValues(3); 260 row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE); 261 row.put(ContactsContract.CommonDataKinds.Nickname.TYPE, 262 ContactsContract.CommonDataKinds.Nickname.TYPE_DEFAULT); 263 row.put(ContactsContract.CommonDataKinds.Nickname.NAME, nickname); 264 data.add(row); 265 break; 266 } 267 } 268 } 269 270 if (!data.isEmpty()) { 271 intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, data); 272 } 273 274 StringBuilder aggregatedNotes = new StringBuilder(); 275 if (note != null) { 276 aggregatedNotes.append('\n').append(note); 277 } 278 if (geo != null && geo.length >= 2) { 279 aggregatedNotes.append('\n').append(geo[0]).append(',').append(geo[1]); 280 } 281 282 if (aggregatedNotes.length() > 0) { 283 // Remove extra leading '\n' 284 putExtra(intent, ContactsContract.Intents.Insert.NOTES, aggregatedNotes.substring(1)); 285 } 286 287 if (instantMessenger != null && instantMessenger.startsWith("xmpp:")) { 288 intent.putExtra(ContactsContract.Intents.Insert.IM_PROTOCOL, ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER); 289 intent.putExtra(ContactsContract.Intents.Insert.IM_HANDLE, instantMessenger.substring(5)); 290 } else { 291 putExtra(intent, ContactsContract.Intents.Insert.IM_HANDLE, instantMessenger); 292 } 293 294 putExtra(intent, ContactsContract.Intents.Insert.POSTAL, address); 295 if (addressType != null) { 296 int type = toAddressContractType(addressType); 297 if (type >= 0) { 298 intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, type); 299 } 300 } 301 putExtra(intent, ContactsContract.Intents.Insert.COMPANY, org); 302 putExtra(intent, ContactsContract.Intents.Insert.JOB_TITLE, title); 303 launchIntent(intent); 304 } 305 toEmailContractType(String typeString)306 private static int toEmailContractType(String typeString) { 307 return doToContractType(typeString, EMAIL_TYPE_STRINGS, EMAIL_TYPE_VALUES); 308 } 309 toPhoneContractType(String typeString)310 private static int toPhoneContractType(String typeString) { 311 return doToContractType(typeString, PHONE_TYPE_STRINGS, PHONE_TYPE_VALUES); 312 } 313 toAddressContractType(String typeString)314 private static int toAddressContractType(String typeString) { 315 return doToContractType(typeString, ADDRESS_TYPE_STRINGS, ADDRESS_TYPE_VALUES); 316 } 317 doToContractType(String typeString, String[] types, int[] values)318 private static int doToContractType(String typeString, String[] types, int[] values) { 319 if (typeString == null) { 320 return NO_TYPE; 321 } 322 for (int i = 0; i < types.length; i++) { 323 String type = types[i]; 324 if (typeString.startsWith(type) || typeString.startsWith(type.toUpperCase(Locale.ENGLISH))) { 325 return values[i]; 326 } 327 } 328 return NO_TYPE; 329 } 330 shareByEmail(String contents)331 final void shareByEmail(String contents) { 332 sendEmail(null, null, null, null, contents); 333 } 334 sendEmail(String[] to, String[] cc, String[] bcc, String subject, String body)335 final void sendEmail(String[] to, 336 String[] cc, 337 String[] bcc, 338 String subject, 339 String body) { 340 Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:")); 341 if (to != null && to.length != 0) { 342 intent.putExtra(Intent.EXTRA_EMAIL, to); 343 } 344 if (cc != null && cc.length != 0) { 345 intent.putExtra(Intent.EXTRA_CC, cc); 346 } 347 if (bcc != null && bcc.length != 0) { 348 intent.putExtra(Intent.EXTRA_BCC, bcc); 349 } 350 putExtra(intent, Intent.EXTRA_SUBJECT, subject); 351 putExtra(intent, Intent.EXTRA_TEXT, body); 352 intent.setType("text/plain"); 353 launchIntent(intent); 354 } 355 shareBySMS(String contents)356 final void shareBySMS(String contents) { 357 sendSMSFromUri("smsto:", contents); 358 } 359 sendSMS(String phoneNumber, String body)360 final void sendSMS(String phoneNumber, String body) { 361 sendSMSFromUri("smsto:" + phoneNumber, body); 362 } 363 sendSMSFromUri(String uri, String body)364 private void sendSMSFromUri(String uri, String body) { 365 Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri)); 366 putExtra(intent, "sms_body", body); 367 // Exit the app once the SMS is sent 368 intent.putExtra("compose_mode", true); 369 launchIntent(intent); 370 } 371 sendMMS(String phoneNumber, String subject, String body)372 final void sendMMS(String phoneNumber, String subject, String body) { 373 sendMMSFromUri("mmsto:" + phoneNumber, subject, body); 374 } 375 sendMMSFromUri(String uri, String subject, String body)376 private void sendMMSFromUri(String uri, String subject, String body) { 377 Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri)); 378 // The Messaging app needs to see a valid subject or else it will treat this an an SMS. 379 if (subject == null || subject.isEmpty()) { 380 putExtra(intent, "subject", activity.getString(R.string.msg_default_mms_subject)); 381 } else { 382 putExtra(intent, "subject", subject); 383 } 384 putExtra(intent, "sms_body", body); 385 intent.putExtra("compose_mode", true); 386 launchIntent(intent); 387 } 388 dialPhone(String phoneNumber)389 final void dialPhone(String phoneNumber) { 390 launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber))); 391 } 392 dialPhoneFromUri(String uri)393 final void dialPhoneFromUri(String uri) { 394 launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse(uri))); 395 } 396 openMap(String geoURI)397 final void openMap(String geoURI) { 398 launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(geoURI))); 399 } 400 401 /** 402 * Do a geo search using the address as the query. 403 * 404 * @param address The address to find 405 */ searchMap(String address)406 final void searchMap(String address) { 407 launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=" + Uri.encode(address)))); 408 } 409 getDirections(double latitude, double longitude)410 final void getDirections(double latitude, double longitude) { 411 launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("http://maps.google." + 412 LocaleManager.getCountryTLD(activity) + "/maps?f=d&daddr=" + latitude + ',' + longitude))); 413 } 414 415 // Uses the mobile-specific version of Product Search, which is formatted for small screens. openProductSearch(String upc)416 final void openProductSearch(String upc) { 417 Uri uri = Uri.parse("http://www.google." + LocaleManager.getProductSearchCountryTLD(activity) + 418 "/m/products?q=" + upc + "&source=zxing"); 419 launchIntent(new Intent(Intent.ACTION_VIEW, uri)); 420 } 421 openBookSearch(String isbn)422 final void openBookSearch(String isbn) { 423 Uri uri = Uri.parse("http://books.google." + LocaleManager.getBookSearchCountryTLD(activity) + 424 "/books?vid=isbn" + isbn); 425 launchIntent(new Intent(Intent.ACTION_VIEW, uri)); 426 } 427 searchBookContents(String isbnOrUrl)428 final void searchBookContents(String isbnOrUrl) { 429 Intent intent = new Intent(Intents.SearchBookContents.ACTION); 430 intent.setClassName(activity, SearchBookContentsActivity.class.getName()); 431 putExtra(intent, Intents.SearchBookContents.ISBN, isbnOrUrl); 432 launchIntent(intent); 433 } 434 openURL(String url)435 final void openURL(String url) { 436 // Strangely, some Android browsers don't seem to register to handle HTTP:// or HTTPS://. 437 // Lower-case these as it should always be OK to lower-case these schemes. 438 if (url.startsWith("HTTP://")) { 439 url = "http" + url.substring(4); 440 } else if (url.startsWith("HTTPS://")) { 441 url = "https" + url.substring(5); 442 } 443 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 444 try { 445 launchIntent(intent); 446 } catch (ActivityNotFoundException ignored) { 447 Log.w(TAG, "Nothing available to handle " + intent); 448 } 449 } 450 webSearch(String query)451 final void webSearch(String query) { 452 Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 453 intent.putExtra("query", query); 454 launchIntent(intent); 455 } 456 457 /** 458 * Like {@link #launchIntent(Intent)} but will tell you if it is not handle-able 459 * via {@link ActivityNotFoundException}. 460 * 461 * @throws ActivityNotFoundException if Intent can't be handled 462 */ rawLaunchIntent(Intent intent)463 final void rawLaunchIntent(Intent intent) { 464 if (intent != null) { 465 intent.addFlags(Intents.FLAG_NEW_DOC); 466 activity.startActivity(intent); 467 } 468 } 469 470 /** 471 * Like {@link #rawLaunchIntent(Intent)} but will show a user dialog if nothing is available to handle. 472 */ launchIntent(Intent intent)473 final void launchIntent(Intent intent) { 474 try { 475 rawLaunchIntent(intent); 476 } catch (ActivityNotFoundException ignored) { 477 AlertDialog.Builder builder = new AlertDialog.Builder(activity); 478 builder.setTitle(R.string.app_name); 479 builder.setMessage(R.string.msg_intent_failed); 480 builder.setPositiveButton(R.string.button_ok, null); 481 builder.show(); 482 } 483 } 484 putExtra(Intent intent, String key, String value)485 private static void putExtra(Intent intent, String key, String value) { 486 if (value != null && !value.isEmpty()) { 487 intent.putExtra(key, value); 488 } 489 } 490 parseCustomSearchURL()491 private String parseCustomSearchURL() { 492 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); 493 String customProductSearch = prefs.getString(PreferencesActivity.KEY_CUSTOM_PRODUCT_SEARCH, 494 null); 495 if (customProductSearch != null && customProductSearch.trim().isEmpty()) { 496 return null; 497 } 498 return customProductSearch; 499 } 500 fillInCustomSearchURL(String text)501 final String fillInCustomSearchURL(String text) { 502 if (customProductSearch == null) { 503 return text; // ? 504 } 505 try { 506 text = URLEncoder.encode(text, "UTF-8"); 507 } catch (UnsupportedEncodingException e) { 508 // can't happen; UTF-8 is always supported. Continue, I guess, without encoding 509 } 510 String url = customProductSearch; 511 if (rawResult != null) { 512 // Replace %f but only if it doesn't seem to be a hex escape sequence. This remains 513 // problematic but avoids the more surprising problem of breaking escapes 514 url = url.replaceFirst("%f(?![0-9a-f])", rawResult.getBarcodeFormat().toString()); 515 if (url.contains("%t")) { 516 ParsedResult parsedResultAgain = ResultParser.parseResult(rawResult); 517 url = url.replace("%t", parsedResultAgain.getType().toString()); 518 } 519 } 520 // Replace %s last as it might contain itself %f or %t 521 return url.replace("%s", text); 522 } 523 524 @SuppressWarnings("deprecation") formatPhone(String phoneData)525 static String formatPhone(String phoneData) { 526 // Just collect the call to a deprecated method in one place 527 return PhoneNumberUtils.formatNumber(phoneData); 528 } 529 530 } 531