xref: /aosp_15_r20/external/zxing/android/src/com/google/zxing/client/android/result/ResultHandler.java (revision 513427e33d61bc67fc40bc261642ac0b2a686b45)
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