8cf6693e66ecfbc4ccca703a5b878cb776dc649f
[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     }
116     if (format == null || BarcodeFormat.QR_CODE.equals(format)) {
117       String type = intent.getStringExtra(Intents.Encode.TYPE);
118       if (type == null || type.length() == 0) {
119         return false;
120       }
121       this.format = BarcodeFormat.QR_CODE;
122       encodeQRCodeContents(intent, type);
123     } else {
124       String data = intent.getStringExtra(Intents.Encode.DATA);
125       if (data != null && data.length() > 0) {
126         contents = data;
127         displayContents = data;
128         title = activity.getString(R.string.contents_text);
129       }
130     }
131     return contents != null && contents.length() > 0;
132   }
133
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;
137     try {
138       Uri uri = (Uri)intent.getExtras().getParcelable(Intent.EXTRA_STREAM);
139       InputStream stream = activity.getContentResolver().openInputStream(uri);
140       int length = stream.available();
141       if (length <= 0) {
142         Log.w(TAG, "Content stream is empty");
143         return false;
144       }
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");
149         return false;
150       }
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");
158         return false;
159       }
160       if (!encodeQRCodeContents((AddressBookParsedResult) parsedResult)) {
161         Log.d(TAG, "Unable to encode contents");
162         return false;
163       }
164     } catch (IOException e) {
165       Log.w(TAG, e);
166       return false;
167     } catch (NullPointerException e) {
168       Log.w(TAG, e);
169       // In case the uri was not found in the Intent.
170       return false;
171     }
172     return contents != null && contents.length() > 0;
173   }
174
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) {
179         contents = data;
180         displayContents = data;
181         title = activity.getString(R.string.contents_text);
182       }
183     } else if (type.equals(Contents.Type.EMAIL)) {
184       String data = trim(intent.getStringExtra(Intents.Encode.DATA));
185       if (data != null) {
186         contents = "mailto:" + data;
187         displayContents = data;
188         title = activity.getString(R.string.contents_email);
189       }
190     } else if (type.equals(Contents.Type.PHONE)) {
191       String data = trim(intent.getStringExtra(Intents.Encode.DATA));
192       if (data != null) {
193         contents = "tel:" + data;
194         displayContents = PhoneNumberUtils.formatNumber(data);
195         title = activity.getString(R.string.contents_phone);
196       }
197     } else if (type.equals(Contents.Type.SMS)) {
198       String data = trim(intent.getStringExtra(Intents.Encode.DATA));
199       if (data != null) {
200         contents = "sms:" + data;
201         displayContents = PhoneNumberUtils.formatNumber(data);
202         title = activity.getString(R.string.contents_sms);
203       }
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));
211         if (name != null) {
212           newContents.append("N:").append(escapeMECARD(name)).append(';');
213           newDisplayContents.append(name);
214         }
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);
219         }
220         for (int x = 0; x < Contents.PHONE_KEYS.length; x++) {
221           String phone = trim(bundle.getString(Contents.PHONE_KEYS[x]));
222           if (phone != null) {
223             newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
224             newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
225           }
226         }
227         for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) {
228           String email = trim(bundle.getString(Contents.EMAIL_KEYS[x]));
229           if (email != null) {
230             newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
231             newDisplayContents.append('\n').append(email);
232           }
233         }
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);
240         } else {
241           contents = null;
242           displayContents = null;
243         }
244       }
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);
255         }
256       }
257     }
258   }
259
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]);
267       if (name != null) {
268         newContents.append("N:").append(escapeMECARD(name)).append(';');
269         newDisplayContents.append(name);
270       }
271     }
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);
279         }
280       }
281     }
282     String[] phoneNumbers = contact.getPhoneNumbers();
283     if (phoneNumbers != null) {
284       for (String phone : phoneNumbers) {
285         phone = trim(phone);
286         if (phone != null) {
287           newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
288           newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
289         }
290       }
291     }
292     String[] emails = contact.getEmails();
293     if (emails != null) {
294       for (String email : emails) {
295         email = trim(email);
296         if (email != null) {
297           newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
298           newDisplayContents.append('\n').append(email);
299         }
300       }
301     }
302     String url = trim(contact.getURL());
303     if (url != null) {
304       newContents.append("URL:").append(escapeMECARD(url)).append(';');
305       newDisplayContents.append('\n').append(url);
306     }
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);
313       return true;
314     } else {
315       contents = null;
316       displayContents = null;
317       return false;
318     }
319   }
320
321   static Bitmap encodeAsBitmap(String contents,
322                                BarcodeFormat format,
323                                int desiredWidth,
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);
330     }
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;
341       }
342     }
343
344     Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
345     bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
346     return bitmap;
347   }
348
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) {
353         return "UTF-8";
354       }
355     }
356     return null;
357   }
358
359   private static String trim(String s) {
360     if (s == null) {
361       return null;
362     }
363     s = s.trim();
364     return s.length() == 0 ? null : s;
365   }
366
367   private static String escapeMECARD(String input) {
368     if (input == null || (input.indexOf(':') < 0 && input.indexOf(';') < 0)) {
369       return input;
370     }
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 == ';') {
376         result.append('\\');
377       }
378       result.append(c);
379     }
380     return result.toString();
381   }
382
383 }