| /* |
| * Copyright (C) 2008 ZXing authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.zxing.client.android.encode; |
| |
| import com.google.zxing.BarcodeFormat; |
| import com.google.zxing.EncodeHintType; |
| import com.google.zxing.MultiFormatWriter; |
| import com.google.zxing.Result; |
| import com.google.zxing.WriterException; |
| import com.google.zxing.client.android.Contents; |
| import com.google.zxing.client.android.Intents; |
| import com.google.zxing.client.android.R; |
| import com.google.zxing.client.result.AddressBookParsedResult; |
| import com.google.zxing.client.result.ParsedResult; |
| import com.google.zxing.client.result.ResultParser; |
| import com.google.zxing.common.BitMatrix; |
| |
| import android.app.Activity; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.Contacts; |
| import android.telephony.PhoneNumberUtils; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| |
| /** |
| * This class does the work of decoding the user's request and extracting all the data |
| * to be encoded in a barcode. |
| * |
| * @author dswitkin@google.com (Daniel Switkin) |
| */ |
| final class QRCodeEncoder { |
| |
| private static final String TAG = QRCodeEncoder.class.getSimpleName(); |
| |
| private static final int WHITE = 0xFFFFFFFF; |
| private static final int BLACK = 0xFF000000; |
| |
| private final Activity activity; |
| private String contents; |
| private String displayContents; |
| private String title; |
| private BarcodeFormat format; |
| private final int dimension; |
| |
| QRCodeEncoder(Activity activity, Intent intent, int dimension) { |
| this.activity = activity; |
| if (intent == null) { |
| throw new IllegalArgumentException("No valid data to encode."); |
| } |
| |
| String action = intent.getAction(); |
| if (action.equals(Intents.Encode.ACTION)) { |
| if (!encodeContentsFromZXingIntent(intent)) { |
| throw new IllegalArgumentException("No valid data to encode."); |
| } |
| } else if (action.equals(Intent.ACTION_SEND)) { |
| if (!encodeContentsFromShareIntent(intent)) { |
| throw new IllegalArgumentException("No valid data to encode."); |
| } |
| } |
| |
| this.dimension = dimension; |
| } |
| |
| public String getContents() { |
| return contents; |
| } |
| |
| public String getDisplayContents() { |
| return displayContents; |
| } |
| |
| public String getTitle() { |
| return title; |
| } |
| |
| // It would be nice if the string encoding lived in the core ZXing library, |
| // but we use platform specific code like PhoneNumberUtils, so it can't. |
| private boolean encodeContentsFromZXingIntent(Intent intent) { |
| // Default to QR_CODE if no format given. |
| String formatString = intent.getStringExtra(Intents.Encode.FORMAT); |
| try { |
| format = BarcodeFormat.valueOf(formatString); |
| } catch (IllegalArgumentException iae) { |
| // Ignore it then |
| format = null; |
| } |
| if (format == null || BarcodeFormat.QR_CODE.equals(format)) { |
| String type = intent.getStringExtra(Intents.Encode.TYPE); |
| if (type == null || type.length() == 0) { |
| return false; |
| } |
| this.format = BarcodeFormat.QR_CODE; |
| encodeQRCodeContents(intent, type); |
| } else { |
| String data = intent.getStringExtra(Intents.Encode.DATA); |
| if (data != null && data.length() > 0) { |
| contents = data; |
| displayContents = data; |
| title = activity.getString(R.string.contents_text); |
| } |
| } |
| return contents != null && contents.length() > 0; |
| } |
| |
| // Handles send intents from multitude of Android applications |
| private boolean encodeContentsFromShareIntent(Intent intent) { |
| // Check if this is a plain text encoding, or contact |
| if (intent.hasExtra(Intent.EXTRA_TEXT)) { |
| return encodeContentsFromShareIntentPlainText(intent); |
| } |
| // Attempt default sharing. |
| return encodeContentsFromShareIntentDefault(intent); |
| } |
| |
| private boolean encodeContentsFromShareIntentPlainText(Intent intent) { |
| // Notice: Google Maps shares both URL and details in one text, bummer! |
| contents = intent.getStringExtra(Intent.EXTRA_TEXT); |
| // We only support non-empty and non-blank texts. |
| // Trim text to avoid URL breaking. |
| if (contents == null) { |
| return false; |
| } |
| contents = contents.trim(); |
| if (contents.length() == 0) { |
| return false; |
| } |
| // We only do QR code. |
| format = BarcodeFormat.QR_CODE; |
| if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { |
| displayContents = intent.getStringExtra(Intent.EXTRA_SUBJECT); |
| } else if (intent.hasExtra(Intent.EXTRA_TITLE)) { |
| displayContents = intent.getStringExtra(Intent.EXTRA_TITLE); |
| } else { |
| displayContents = contents; |
| } |
| title = activity.getString(R.string.contents_text); |
| return true; |
| } |
| |
| // Handles send intents from the Contacts app, retrieving a contact as a VCARD. |
| // Note: Does not work on HTC devices due to broken custom Contacts application. |
| private boolean encodeContentsFromShareIntentDefault(Intent intent) { |
| format = BarcodeFormat.QR_CODE; |
| try { |
| Uri uri = (Uri)intent.getExtras().getParcelable(Intent.EXTRA_STREAM); |
| InputStream stream = activity.getContentResolver().openInputStream(uri); |
| int length = stream.available(); |
| if (length <= 0) { |
| Log.w(TAG, "Content stream is empty"); |
| return false; |
| } |
| byte[] vcard = new byte[length]; |
| int bytesRead = stream.read(vcard, 0, length); |
| if (bytesRead < length) { |
| Log.w(TAG, "Unable to fully read available bytes from content stream"); |
| return false; |
| } |
| String vcardString = new String(vcard, 0, bytesRead, "UTF-8"); |
| Log.d(TAG, "Encoding share intent content:"); |
| Log.d(TAG, vcardString); |
| Result result = new Result(vcardString, vcard, null, BarcodeFormat.QR_CODE); |
| ParsedResult parsedResult = ResultParser.parseResult(result); |
| if (!(parsedResult instanceof AddressBookParsedResult)) { |
| Log.d(TAG, "Result was not an address"); |
| return false; |
| } |
| if (!encodeQRCodeContents((AddressBookParsedResult) parsedResult)) { |
| Log.d(TAG, "Unable to encode contents"); |
| return false; |
| } |
| } catch (IOException e) { |
| Log.w(TAG, e); |
| return false; |
| } catch (NullPointerException e) { |
| Log.w(TAG, e); |
| // In case the uri was not found in the Intent. |
| return false; |
| } |
| return contents != null && contents.length() > 0; |
| } |
| |
| private void encodeQRCodeContents(Intent intent, String type) { |
| if (type.equals(Contents.Type.TEXT)) { |
| String data = intent.getStringExtra(Intents.Encode.DATA); |
| if (data != null && data.length() > 0) { |
| contents = data; |
| displayContents = data; |
| title = activity.getString(R.string.contents_text); |
| } |
| } else if (type.equals(Contents.Type.EMAIL)) { |
| String data = trim(intent.getStringExtra(Intents.Encode.DATA)); |
| if (data != null) { |
| contents = "mailto:" + data; |
| displayContents = data; |
| title = activity.getString(R.string.contents_email); |
| } |
| } else if (type.equals(Contents.Type.PHONE)) { |
| String data = trim(intent.getStringExtra(Intents.Encode.DATA)); |
| if (data != null) { |
| contents = "tel:" + data; |
| displayContents = PhoneNumberUtils.formatNumber(data); |
| title = activity.getString(R.string.contents_phone); |
| } |
| } else if (type.equals(Contents.Type.SMS)) { |
| String data = trim(intent.getStringExtra(Intents.Encode.DATA)); |
| if (data != null) { |
| contents = "sms:" + data; |
| displayContents = PhoneNumberUtils.formatNumber(data); |
| title = activity.getString(R.string.contents_sms); |
| } |
| } else if (type.equals(Contents.Type.CONTACT)) { |
| |
| Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA); |
| if (bundle != null) { |
| |
| StringBuilder newContents = new StringBuilder(100); |
| StringBuilder newDisplayContents = new StringBuilder(100); |
| |
| newContents.append("MECARD:"); |
| |
| String name = trim(bundle.getString(Contacts.Intents.Insert.NAME)); |
| if (name != null) { |
| newContents.append("N:").append(escapeMECARD(name)).append(';'); |
| newDisplayContents.append(name); |
| } |
| |
| String address = trim(bundle.getString(Contacts.Intents.Insert.POSTAL)); |
| if (address != null) { |
| newContents.append("ADR:").append(escapeMECARD(address)).append(';'); |
| newDisplayContents.append('\n').append(address); |
| } |
| |
| Collection<String> uniquePhones = new HashSet<String>(Contents.PHONE_KEYS.length); |
| for (int x = 0; x < Contents.PHONE_KEYS.length; x++) { |
| String phone = trim(bundle.getString(Contents.PHONE_KEYS[x])); |
| if (phone != null) { |
| uniquePhones.add(phone); |
| } |
| } |
| for (String phone : uniquePhones) { |
| newContents.append("TEL:").append(escapeMECARD(phone)).append(';'); |
| newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone)); |
| } |
| |
| Collection<String> uniqueEmails = new HashSet<String>(Contents.EMAIL_KEYS.length); |
| for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) { |
| String email = trim(bundle.getString(Contents.EMAIL_KEYS[x])); |
| if (email != null) { |
| uniqueEmails.add(email); |
| } |
| } |
| for (String email : uniqueEmails) { |
| newContents.append("EMAIL:").append(escapeMECARD(email)).append(';'); |
| newDisplayContents.append('\n').append(email); |
| } |
| |
| // Make sure we've encoded at least one field. |
| if (newDisplayContents.length() > 0) { |
| newContents.append(';'); |
| contents = newContents.toString(); |
| displayContents = newDisplayContents.toString(); |
| title = activity.getString(R.string.contents_contact); |
| } else { |
| contents = null; |
| displayContents = null; |
| } |
| |
| } |
| |
| } else if (type.equals(Contents.Type.LOCATION)) { |
| Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA); |
| if (bundle != null) { |
| // These must use Bundle.getFloat(), not getDouble(), it's part of the API. |
| float latitude = bundle.getFloat("LAT", Float.MAX_VALUE); |
| float longitude = bundle.getFloat("LONG", Float.MAX_VALUE); |
| if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) { |
| contents = "geo:" + latitude + ',' + longitude; |
| displayContents = latitude + "," + longitude; |
| title = activity.getString(R.string.contents_location); |
| } |
| } |
| } |
| } |
| |
| private boolean encodeQRCodeContents(AddressBookParsedResult contact) { |
| |
| StringBuilder newContents = new StringBuilder(100); |
| StringBuilder newDisplayContents = new StringBuilder(100); |
| |
| newContents.append("MECARD:"); |
| |
| String[] names = contact.getNames(); |
| if (names != null && names.length > 0) { |
| String name = trim(names[0]); |
| if (name != null) { |
| newContents.append("N:").append(escapeMECARD(name)).append(';'); |
| newDisplayContents.append(name); |
| } |
| } |
| |
| for (String address : trimAndDeduplicate(contact.getAddresses())) { |
| newContents.append("ADR:").append(escapeMECARD(address)).append(';'); |
| newDisplayContents.append('\n').append(address); |
| } |
| |
| for (String phone : trimAndDeduplicate(contact.getPhoneNumbers())) { |
| newContents.append("TEL:").append(escapeMECARD(phone)).append(';'); |
| newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone)); |
| } |
| |
| for (String email : trimAndDeduplicate(contact.getEmails())) { |
| newContents.append("EMAIL:").append(escapeMECARD(email)).append(';'); |
| newDisplayContents.append('\n').append(email); |
| } |
| |
| String url = trim(contact.getURL()); |
| if (url != null) { |
| newContents.append("URL:").append(escapeMECARD(url)).append(';'); |
| newDisplayContents.append('\n').append(url); |
| } |
| |
| // Make sure we've encoded at least one field. |
| if (newDisplayContents.length() > 0) { |
| newContents.append(';'); |
| contents = newContents.toString(); |
| displayContents = newDisplayContents.toString(); |
| title = activity.getString(R.string.contents_contact); |
| return true; |
| } else { |
| contents = null; |
| displayContents = null; |
| return false; |
| } |
| } |
| |
| private static Iterable<String> trimAndDeduplicate(String[] values) { |
| if (values == null || values.length == 0) { |
| return Collections.emptySet(); |
| } |
| Collection<String> uniqueValues = new HashSet<String>(values.length); |
| for (String value : values) { |
| uniqueValues.add(trim(value)); |
| } |
| return uniqueValues; |
| } |
| |
| Bitmap encodeAsBitmap() throws WriterException { |
| Hashtable<EncodeHintType,Object> hints = null; |
| String encoding = guessAppropriateEncoding(contents); |
| if (encoding != null) { |
| hints = new Hashtable<EncodeHintType,Object>(2); |
| hints.put(EncodeHintType.CHARACTER_SET, encoding); |
| } |
| MultiFormatWriter writer = new MultiFormatWriter(); |
| BitMatrix result = writer.encode(contents, format, dimension, dimension, hints); |
| int width = result.getWidth(); |
| int height = result.getHeight(); |
| int[] pixels = new int[width * height]; |
| // All are 0, or black, by default |
| for (int y = 0; y < height; y++) { |
| int offset = y * width; |
| for (int x = 0; x < width; x++) { |
| pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; |
| } |
| } |
| |
| Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); |
| bitmap.setPixels(pixels, 0, width, 0, 0, width, height); |
| return bitmap; |
| } |
| |
| private static String guessAppropriateEncoding(CharSequence contents) { |
| // Very crude at the moment |
| for (int i = 0; i < contents.length(); i++) { |
| if (contents.charAt(i) > 0xFF) { |
| return "UTF-8"; |
| } |
| } |
| return null; |
| } |
| |
| private static String trim(String s) { |
| if (s == null) { |
| return null; |
| } |
| s = s.trim(); |
| return s.length() == 0 ? null : s; |
| } |
| |
| private static String escapeMECARD(String input) { |
| if (input == null || (input.indexOf(':') < 0 && input.indexOf(';') < 0)) { |
| return input; |
| } |
| int length = input.length(); |
| StringBuilder result = new StringBuilder(length); |
| for (int i = 0; i < length; i++) { |
| char c = input.charAt(i); |
| if (c == ':' || c == ';') { |
| result.append('\\'); |
| } |
| result.append(c); |
| } |
| return result.toString(); |
| } |
| |
| } |