2 * Copyright (C) 2008 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.zxing.client.android.encode;
19 import com.google.zxing.BarcodeFormat;
20 import com.google.zxing.EncodeHintType;
21 import com.google.zxing.MultiFormatWriter;
22 import com.google.zxing.Result;
23 import com.google.zxing.WriterException;
24 import com.google.zxing.client.android.Contents;
25 import com.google.zxing.client.android.Intents;
26 import com.google.zxing.client.android.R;
27 import com.google.zxing.client.result.AddressBookParsedResult;
28 import com.google.zxing.client.result.ParsedResult;
29 import com.google.zxing.client.result.ResultParser;
30 import com.google.zxing.common.BitMatrix;
32 import android.app.Activity;
33 import android.content.Intent;
34 import android.graphics.Bitmap;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.provider.Contacts;
39 import android.telephony.PhoneNumberUtils;
40 import android.util.Log;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.util.Hashtable;
47 * This class does the work of decoding the user's request and extracting all the data
48 * to be encoded in a barcode.
50 * @author dswitkin@google.com (Daniel Switkin)
52 final class QRCodeEncoder {
54 private static final String TAG = QRCodeEncoder.class.getSimpleName();
56 private static final int WHITE = 0xFFFFFFFF;
57 private static final int BLACK = 0xFF000000;
59 private final Activity activity;
60 private String contents;
61 private String displayContents;
63 private BarcodeFormat format;
65 QRCodeEncoder(Activity activity, Intent intent) {
66 this.activity = activity;
68 throw new IllegalArgumentException("No valid data to encode.");
71 String action = intent.getAction();
72 if (action.equals(Intents.Encode.ACTION)) {
73 if (!encodeContentsFromZXingIntent(intent)) {
74 throw new IllegalArgumentException("No valid data to encode.");
76 } else if (action.equals(Intent.ACTION_SEND)) {
77 if (!encodeContentsFromShareIntent(intent)) {
78 throw new IllegalArgumentException("No valid data to encode.");
83 public void requestBarcode(Handler handler, int pixelResolution) {
84 Thread encodeThread = new EncodeThread(contents, handler, pixelResolution,
89 public String getContents() {
93 public String getDisplayContents() {
94 return displayContents;
97 public String getTitle() {
101 public String getFormat() {
102 return format.toString();
105 // It would be nice if the string encoding lived in the core ZXing library,
106 // but we use platform specific code like PhoneNumberUtils, so it can't.
107 private boolean encodeContentsFromZXingIntent(Intent intent) {
108 // Default to QR_CODE if no format given.
109 String formatString = intent.getStringExtra(Intents.Encode.FORMAT);
111 format = BarcodeFormat.valueOf(formatString);
112 } catch (IllegalArgumentException iae) {
116 if (format == null || BarcodeFormat.QR_CODE.equals(format)) {
117 String type = intent.getStringExtra(Intents.Encode.TYPE);
118 if (type == null || type.length() == 0) {
121 this.format = BarcodeFormat.QR_CODE;
122 encodeQRCodeContents(intent, type);
124 String data = intent.getStringExtra(Intents.Encode.DATA);
125 if (data != null && data.length() > 0) {
127 displayContents = data;
128 title = activity.getString(R.string.contents_text);
131 return contents != null && contents.length() > 0;
134 // Handles send intents from the Contacts app, retrieving a contact as a VCARD.
135 private boolean encodeContentsFromShareIntent(Intent intent) {
136 format = BarcodeFormat.QR_CODE;
138 Uri uri = (Uri)intent.getExtras().getParcelable(Intent.EXTRA_STREAM);
139 InputStream stream = activity.getContentResolver().openInputStream(uri);
140 int length = stream.available();
142 Log.w(TAG, "Content stream is empty");
145 byte[] vcard = new byte[length];
146 int bytesRead = stream.read(vcard, 0, length);
147 if (bytesRead < length) {
148 Log.w(TAG, "Unable to fully read available bytes from content stream");
151 String vcardString = new String(vcard, 0, bytesRead, "UTF-8");
152 Log.d(TAG, "Encoding share intent content:");
153 Log.d(TAG, vcardString);
154 Result result = new Result(vcardString, vcard, null, BarcodeFormat.QR_CODE);
155 ParsedResult parsedResult = ResultParser.parseResult(result);
156 if (!(parsedResult instanceof AddressBookParsedResult)) {
157 Log.d(TAG, "Result was not an address");
160 if (!encodeQRCodeContents((AddressBookParsedResult) parsedResult)) {
161 Log.d(TAG, "Unable to encode contents");
164 } catch (IOException e) {
167 } catch (NullPointerException e) {
169 // In case the uri was not found in the Intent.
172 return contents != null && contents.length() > 0;
175 private void encodeQRCodeContents(Intent intent, String type) {
176 if (type.equals(Contents.Type.TEXT)) {
177 String data = intent.getStringExtra(Intents.Encode.DATA);
178 if (data != null && data.length() > 0) {
180 displayContents = data;
181 title = activity.getString(R.string.contents_text);
183 } else if (type.equals(Contents.Type.EMAIL)) {
184 String data = trim(intent.getStringExtra(Intents.Encode.DATA));
186 contents = "mailto:" + data;
187 displayContents = data;
188 title = activity.getString(R.string.contents_email);
190 } else if (type.equals(Contents.Type.PHONE)) {
191 String data = trim(intent.getStringExtra(Intents.Encode.DATA));
193 contents = "tel:" + data;
194 displayContents = PhoneNumberUtils.formatNumber(data);
195 title = activity.getString(R.string.contents_phone);
197 } else if (type.equals(Contents.Type.SMS)) {
198 String data = trim(intent.getStringExtra(Intents.Encode.DATA));
200 contents = "sms:" + data;
201 displayContents = PhoneNumberUtils.formatNumber(data);
202 title = activity.getString(R.string.contents_sms);
204 } else if (type.equals(Contents.Type.CONTACT)) {
205 Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA);
206 if (bundle != null) {
207 StringBuilder newContents = new StringBuilder(100);
208 StringBuilder newDisplayContents = new StringBuilder(100);
209 newContents.append("MECARD:");
210 String name = trim(bundle.getString(Contacts.Intents.Insert.NAME));
212 newContents.append("N:").append(escapeMECARD(name)).append(';');
213 newDisplayContents.append(name);
215 String address = trim(bundle.getString(Contacts.Intents.Insert.POSTAL));
216 if (address != null) {
217 newContents.append("ADR:").append(escapeMECARD(address)).append(';');
218 newDisplayContents.append('\n').append(address);
220 for (int x = 0; x < Contents.PHONE_KEYS.length; x++) {
221 String phone = trim(bundle.getString(Contents.PHONE_KEYS[x]));
223 newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
224 newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
227 for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) {
228 String email = trim(bundle.getString(Contents.EMAIL_KEYS[x]));
230 newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
231 newDisplayContents.append('\n').append(email);
234 // Make sure we've encoded at least one field.
235 if (newDisplayContents.length() > 0) {
236 newContents.append(';');
237 contents = newContents.toString();
238 displayContents = newDisplayContents.toString();
239 title = activity.getString(R.string.contents_contact);
242 displayContents = null;
245 } else if (type.equals(Contents.Type.LOCATION)) {
246 Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA);
247 if (bundle != null) {
248 // These must use Bundle.getFloat(), not getDouble(), it's part of the API.
249 float latitude = bundle.getFloat("LAT", Float.MAX_VALUE);
250 float longitude = bundle.getFloat("LONG", Float.MAX_VALUE);
251 if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) {
252 contents = "geo:" + latitude + ',' + longitude;
253 displayContents = latitude + "," + longitude;
254 title = activity.getString(R.string.contents_location);
260 private boolean encodeQRCodeContents(AddressBookParsedResult contact) {
261 StringBuilder newContents = new StringBuilder(100);
262 StringBuilder newDisplayContents = new StringBuilder(100);
263 newContents.append("MECARD:");
264 String[] names = contact.getNames();
265 if (names != null && names.length > 0) {
266 String name = trim(names[0]);
268 newContents.append("N:").append(escapeMECARD(name)).append(';');
269 newDisplayContents.append(name);
272 String[] addresses = contact.getAddresses();
273 if (addresses != null) {
274 for (String address : addresses) {
275 address = trim(address);
276 if (address != null) {
277 newContents.append("ADR:").append(escapeMECARD(address)).append(';');
278 newDisplayContents.append('\n').append(address);
282 String[] phoneNumbers = contact.getPhoneNumbers();
283 if (phoneNumbers != null) {
284 for (String phone : phoneNumbers) {
287 newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
288 newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
292 String[] emails = contact.getEmails();
293 if (emails != null) {
294 for (String email : emails) {
297 newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
298 newDisplayContents.append('\n').append(email);
302 String url = trim(contact.getURL());
304 newContents.append("URL:").append(escapeMECARD(url)).append(';');
305 newDisplayContents.append('\n').append(url);
307 // Make sure we've encoded at least one field.
308 if (newDisplayContents.length() > 0) {
309 newContents.append(';');
310 contents = newContents.toString();
311 displayContents = newDisplayContents.toString();
312 title = activity.getString(R.string.contents_contact);
316 displayContents = null;
321 static Bitmap encodeAsBitmap(String contents,
322 BarcodeFormat format,
324 int desiredHeight) throws WriterException {
325 Hashtable<EncodeHintType,Object> hints = null;
326 String encoding = guessAppropriateEncoding(contents);
327 if (encoding != null) {
328 hints = new Hashtable<EncodeHintType,Object>(2);
329 hints.put(EncodeHintType.CHARACTER_SET, encoding);
331 MultiFormatWriter writer = new MultiFormatWriter();
332 BitMatrix result = writer.encode(contents, format, desiredWidth, desiredHeight, hints);
333 int width = result.getWidth();
334 int height = result.getHeight();
335 int[] pixels = new int[width * height];
336 // All are 0, or black, by default
337 for (int y = 0; y < height; y++) {
338 int offset = y * width;
339 for (int x = 0; x < width; x++) {
340 pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
344 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
345 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
349 private static String guessAppropriateEncoding(CharSequence contents) {
350 // Very crude at the moment
351 for (int i = 0; i < contents.length(); i++) {
352 if (contents.charAt(i) > 0xFF) {
359 private static String trim(String s) {
364 return s.length() == 0 ? null : s;
367 private static String escapeMECARD(String input) {
368 if (input == null || (input.indexOf(':') < 0 && input.indexOf(';') < 0)) {
371 int length = input.length();
372 StringBuilder result = new StringBuilder(length);
373 for (int i = 0; i < length; i++) {
374 char c = input.charAt(i);
375 if (c == ':' || c == ';') {
380 return result.toString();