Tack on a little more logging for debugging assistance
[zxing.git] / android / src / com / google / zxing / client / android / encode / QRCodeEncoder.java
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.encode;
18
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;
31
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;
41
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.util.Hashtable;
45
46 /**
47  * This class does the work of decoding the user's request and extracting all the data
48  * to be encoded in a barcode.
49  *
50  * @author dswitkin@google.com (Daniel Switkin)
51  */
52 final class QRCodeEncoder {
53
54   private static final String TAG = QRCodeEncoder.class.getSimpleName();
55
56   private static final int WHITE = 0xFFFFFFFF;
57   private static final int BLACK = 0xFF000000;
58
59   private final Activity activity;
60   private String contents;
61   private String displayContents;
62   private String title;
63   private BarcodeFormat format;
64
65   QRCodeEncoder(Activity activity, Intent intent) {
66     this.activity = activity;
67     if (intent == null) {
68       throw new IllegalArgumentException("No valid data to encode.");
69     }
70
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.");
75       }
76     } else if (action.equals(Intent.ACTION_SEND)) {
77       if (!encodeContentsFromShareIntent(intent)) {
78         throw new IllegalArgumentException("No valid data to encode.");
79       }
80     }
81   }
82
83   public void requestBarcode(Handler handler, int pixelResolution) {
84     Thread encodeThread = new EncodeThread(contents, handler, pixelResolution,
85         format);
86     encodeThread.start();
87   }
88
89   public String getContents() {
90     return contents;
91   }
92
93   public String getDisplayContents() {
94     return displayContents;
95   }
96
97   public String getTitle() {
98     return title;
99   }
100
101   public String getFormat() {
102     return format.toString();
103   }
104
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);
110     try {
111       format = BarcodeFormat.valueOf(formatString);
112     } catch (IllegalArgumentException iae) {
113       // Ignore it then
114       format = null;
115       formatString = null;
116     }
117     if (format == null || BarcodeFormat.QR_CODE.equals(format)) {
118       String type = intent.getStringExtra(Intents.Encode.TYPE);
119       if (type == null || type.length() == 0) {
120         return false;
121       }
122       this.format = BarcodeFormat.QR_CODE;
123       encodeQRCodeContents(intent, type);
124     } else {
125       String data = intent.getStringExtra(Intents.Encode.DATA);
126       if (data != null && data.length() != 0) {
127         contents = data;
128         displayContents = data;
129         title = activity.getString(R.string.contents_text);
130       }
131     }
132     return contents != null && contents.length() > 0;
133   }
134
135   // Handles send intents from the Contacts app, retrieving a contact as a VCARD.
136   private boolean encodeContentsFromShareIntent(Intent intent) {
137     format = BarcodeFormat.QR_CODE;
138     try {
139       Uri uri = (Uri)intent.getExtras().getParcelable(Intent.EXTRA_STREAM);
140       InputStream stream = activity.getContentResolver().openInputStream(uri);
141       int length = stream.available();
142       byte[] vcard = new byte[length];
143       int bytesRead = stream.read(vcard, 0, length);
144       String vcardString = new String(vcard, 0, bytesRead, "UTF-8");
145       Log.d(TAG, "Encoding share intent content:");
146       Log.d(TAG, vcardString);
147       Result result = new Result(vcardString, vcard, null, BarcodeFormat.QR_CODE);
148       ParsedResult parsedResult = ResultParser.parseResult(result);
149       if (!(parsedResult instanceof AddressBookParsedResult)) {
150         Log.d(TAG, "Result was not an address");
151         return false;
152       }
153       if (!encodeQRCodeContents((AddressBookParsedResult) parsedResult)) {
154         Log.d(TAG, "Unable to encode contents");
155         return false;
156       }
157     } catch (IOException e) {
158       Log.w(TAG, e);
159       return false;
160     } catch (NullPointerException e) {
161       Log.w(TAG, e);
162       // In case the uri was not found in the Intent.
163       return false;
164     }
165     return contents != null && contents.length() > 0;
166   }
167
168   private void encodeQRCodeContents(Intent intent, String type) {
169     if (type.equals(Contents.Type.TEXT)) {
170       String data = intent.getStringExtra(Intents.Encode.DATA);
171       if (data != null && data.length() > 0) {
172         contents = data;
173         displayContents = data;
174         title = activity.getString(R.string.contents_text);
175       }
176     } else if (type.equals(Contents.Type.EMAIL)) {
177       String data = intent.getStringExtra(Intents.Encode.DATA);
178       if (data != null && data.length() > 0) {
179         contents = "mailto:" + data;
180         displayContents = data;
181         title = activity.getString(R.string.contents_email);
182       }
183     } else if (type.equals(Contents.Type.PHONE)) {
184       String data = intent.getStringExtra(Intents.Encode.DATA);
185       if (data != null && data.length() > 0) {
186         contents = "tel:" + data;
187         displayContents = PhoneNumberUtils.formatNumber(data);
188         title = activity.getString(R.string.contents_phone);
189       }
190     } else if (type.equals(Contents.Type.SMS)) {
191       String data = intent.getStringExtra(Intents.Encode.DATA);
192       if (data != null && data.length() > 0) {
193         contents = "sms:" + data;
194         displayContents = PhoneNumberUtils.formatNumber(data);
195         title = activity.getString(R.string.contents_sms);
196       }
197     } else if (type.equals(Contents.Type.CONTACT)) {
198       Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA);
199       if (bundle != null) {
200         StringBuilder newContents = new StringBuilder();
201         StringBuilder newDisplayContents = new StringBuilder();
202         newContents.append("MECARD:");
203         String name = bundle.getString(Contacts.Intents.Insert.NAME);
204         if (name != null && name.length() > 0) {
205           newContents.append("N:").append(name).append(';');
206           newDisplayContents.append(name);
207         }
208         String address = bundle.getString(Contacts.Intents.Insert.POSTAL);
209         if (address != null && address.length() > 0) {
210           newContents.append("ADR:").append(address).append(';');
211           newDisplayContents.append('\n').append(address);
212         }
213         for (int x = 0; x < Contents.PHONE_KEYS.length; x++) {
214           String phone = bundle.getString(Contents.PHONE_KEYS[x]);
215           if (phone != null && phone.length() > 0) {
216             newContents.append("TEL:").append(phone).append(';');
217             newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
218           }
219         }
220         for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) {
221           String email = bundle.getString(Contents.EMAIL_KEYS[x]);
222           if (email != null && email.length() > 0) {
223             newContents.append("EMAIL:").append(email).append(';');
224             newDisplayContents.append('\n').append(email);
225           }
226         }
227         // Make sure we've encoded at least one field.
228         if (newDisplayContents.length() > 0) {
229           newContents.append(';');
230           contents = newContents.toString();
231           displayContents = newDisplayContents.toString();
232           title = activity.getString(R.string.contents_contact);
233         } else {
234           contents = null;
235           displayContents = null;
236         }
237       }
238     } else if (type.equals(Contents.Type.LOCATION)) {
239       Bundle bundle = intent.getBundleExtra(Intents.Encode.DATA);
240       if (bundle != null) {
241         // These must use Bundle.getFloat(), not getDouble(), it's part of the API.
242         float latitude = bundle.getFloat("LAT", Float.MAX_VALUE);
243         float longitude = bundle.getFloat("LONG", Float.MAX_VALUE);
244         if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) {
245           contents = "geo:" + latitude + ',' + longitude;
246           displayContents = latitude + "," + longitude;
247           title = activity.getString(R.string.contents_location);
248         }
249       }
250     }
251   }
252
253   private boolean encodeQRCodeContents(AddressBookParsedResult contact) {
254     StringBuilder newContents = new StringBuilder();
255     StringBuilder newDisplayContents = new StringBuilder();
256     newContents.append("MECARD:");
257     String[] names = contact.getNames();
258     if (names != null && names.length > 0) {
259       newContents.append("N:").append(names[0]).append(';');
260       newDisplayContents.append(names[0]);
261     }
262     String[] addresses = contact.getAddresses();
263     if (addresses != null) {
264       for (String address : addresses) {
265         if (address != null && address.length() > 0) {
266           newContents.append("ADR:").append(address).append(';');
267           newDisplayContents.append('\n').append(address);
268         }
269       }
270     }
271     String[] phoneNumbers = contact.getPhoneNumbers();
272     if (phoneNumbers != null) {
273       for (String phone : phoneNumbers) {
274         if (phone != null && phone.length() > 0) {
275           newContents.append("TEL:").append(phone).append(';');
276           newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
277         }
278       }
279     }
280     String[] emails = contact.getEmails();
281     if (emails != null) {
282       for (String email : emails) {
283         if (email != null && email.length() > 0) {
284           newContents.append("EMAIL:").append(email).append(';');
285           newDisplayContents.append('\n').append(email);
286         }
287       }
288     }
289     String url = contact.getURL();
290     if (url != null && url.length() > 0) {
291       newContents.append("URL:").append(url).append(';');
292       newDisplayContents.append('\n').append(url);
293     }
294     // Make sure we've encoded at least one field.
295     if (newDisplayContents.length() > 0) {
296       newContents.append(';');
297       contents = newContents.toString();
298       displayContents = newDisplayContents.toString();
299       title = activity.getString(R.string.contents_contact);
300       return true;
301     } else {
302       contents = null;
303       displayContents = null;
304       return false;
305     }
306   }
307
308   static Bitmap encodeAsBitmap(String contents,
309                                BarcodeFormat format,
310                                int desiredWidth,
311                                int desiredHeight) throws WriterException {
312     Hashtable hints = null;
313     String encoding = guessAppropriateEncoding(contents);
314     if (encoding != null) {
315       hints = new Hashtable(2);
316       hints.put(EncodeHintType.CHARACTER_SET, encoding);
317     }
318     MultiFormatWriter writer = new MultiFormatWriter();    
319     BitMatrix result = writer.encode(contents, format, desiredWidth, desiredHeight, hints);
320     int width = result.getWidth();
321     int height = result.getHeight();
322     int[] pixels = new int[width * height];
323     // All are 0, or black, by default
324     for (int y = 0; y < height; y++) {
325       int offset = y * width;
326       for (int x = 0; x < width; x++) {
327         pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
328       }
329     }
330
331     Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
332     bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
333     return bitmap;
334   }
335
336   private static String guessAppropriateEncoding(CharSequence contents) {
337     // Very crude at the moment
338     for (int i = 0; i < contents.length(); i++) {
339       if (contents.charAt(i) > 0xFF) {
340         return "UTF-8";
341       }
342     }
343     return null;
344   }
345 }