Better handling of empty/incomplete content stream
[zxing.git] / android / src / com / google / zxing / client / android / encode / QRCodeEncoder.java
index 2f4a573..ab2e421 100755 (executable)
 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.Intents;
 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.common.ByteMatrix;
+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.os.Handler;
-import android.os.Message;
 import android.provider.Contacts;
 import android.telephony.PhoneNumberUtils;
 import android.util.Log;
 
+import java.io.IOException;
+import java.io.InputStream;
+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 QR Code.
+ * 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;
 
-  public QRCodeEncoder(Activity activity, Intent intent) {
+  QRCodeEncoder(Activity activity, Intent intent) {
     this.activity = activity;
-    if (!encodeContents(intent)) {
+    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.");
+      }
+    }
   }
 
   public void requestBarcode(Handler handler, int pixelResolution) {
@@ -71,22 +97,24 @@ final class QRCodeEncoder {
   public String getTitle() {
     return title;
   }
-  
+
   public String getFormat() {
     return format.toString();
   }
 
   // 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 encodeContents(Intent intent) {
-    if (intent == null) {
-      return false;
+  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;
+      formatString = null;
     }
-    
-    // default to QR_CODE if no format given
-    String format = intent.getStringExtra(Intents.Encode.FORMAT);
-    if (format == null || format.length() == 0 || 
-        format.equals(Contents.Format.QR_CODE)) {
+    if (format == null || BarcodeFormat.QR_CODE.equals(format)) {
       String type = intent.getStringExtra(Intents.Encode.TYPE);
       if (type == null || type.length() == 0) {
         return false;
@@ -99,24 +127,52 @@ final class QRCodeEncoder {
         contents = data;
         displayContents = data;
         title = activity.getString(R.string.contents_text);
-        if (format.equals(Contents.Format.CODE_128)) {
-          this.format = BarcodeFormat.CODE_128;
-        } else if (format.equals(Contents.Format.CODE_39)) {
-          this.format = BarcodeFormat.CODE_39;
-        } else if (format.equals(Contents.Format.EAN_8)) {
-          this.format = BarcodeFormat.EAN_8;
-        } else if (format.equals(Contents.Format.EAN_13)) {
-          this.format = BarcodeFormat.EAN_13;
-        } else if (format.equals(Contents.Format.UPC_A)) {
-          this.format = BarcodeFormat.UPC_A;
-        } else if (format.equals(Contents.Format.UPC_E)) {
-          this.format = BarcodeFormat.UPC_E;
-        }
       }
     }
     return contents != null && contents.length() > 0;
   }
 
+  // Handles send intents from the Contacts app, retrieving a contact as a VCARD.
+  private boolean encodeContentsFromShareIntent(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);
@@ -202,53 +258,96 @@ final class QRCodeEncoder {
     }
   }
 
-  private static final class EncodeThread extends Thread {
-    private static final String TAG = "EncodeThread";
-
-    private final String contents;
-    private final Handler handler;
-    private final int pixelResolution;
-    private final BarcodeFormat format;
+  private boolean encodeQRCodeContents(AddressBookParsedResult contact) {
+    StringBuilder newContents = new StringBuilder();
+    StringBuilder newDisplayContents = new StringBuilder();
+    newContents.append("MECARD:");
+    String[] names = contact.getNames();
+    if (names != null && names.length > 0) {
+      newContents.append("N:").append(names[0]).append(';');
+      newDisplayContents.append(names[0]);
+    }
+    String[] addresses = contact.getAddresses();
+    if (addresses != null) {
+      for (String address : addresses) {
+        if (address != null && address.length() > 0) {
+          newContents.append("ADR:").append(address).append(';');
+          newDisplayContents.append('\n').append(address);
+        }
+      }
+    }
+    String[] phoneNumbers = contact.getPhoneNumbers();
+    if (phoneNumbers != null) {
+      for (String phone : phoneNumbers) {
+        if (phone != null && phone.length() > 0) {
+          newContents.append("TEL:").append(phone).append(';');
+          newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
+        }
+      }
+    }
+    String[] emails = contact.getEmails();
+    if (emails != null) {
+      for (String email : emails) {
+        if (email != null && email.length() > 0) {
+          newContents.append("EMAIL:").append(email).append(';');
+          newDisplayContents.append('\n').append(email);
+        }
+      }
+    }
+    String url = contact.getURL();
+    if (url != null && url.length() > 0) {
+      newContents.append("URL:").append(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;
+    }
+  }
 
-    EncodeThread(String contents, Handler handler, int pixelResolution,
-        BarcodeFormat format) {
-      this.contents = contents;
-      this.handler = handler;
-      this.pixelResolution = pixelResolution;
-      this.format = format;
+  static Bitmap encodeAsBitmap(String contents,
+                               BarcodeFormat format,
+                               int desiredWidth,
+                               int desiredHeight) throws WriterException {
+    Hashtable hints = null;
+    String encoding = guessAppropriateEncoding(contents);
+    if (encoding != null) {
+      hints = new Hashtable(2);
+      hints.put(EncodeHintType.CHARACTER_SET, encoding);
+    }
+    MultiFormatWriter writer = new MultiFormatWriter();    
+    BitMatrix result = writer.encode(contents, format, desiredWidth, desiredHeight, 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;
+      }
     }
 
-    @Override
-    public void run() {
-      try {
-        ByteMatrix result = new MultiFormatWriter().encode(contents, format,
-            pixelResolution, pixelResolution);
-        int width = result.getWidth();
-        int height = result.getHeight();
-        byte[][] array = result.getArray();
-        int[] pixels = new int[width * height];
-        for (int y = 0; y < height; y++) {
-          for (int x = 0; x < width; x++) {
-            int grey = array[y][x] & 0xff;
-            // pixels[y * width + x] = (0xff << 24) | (grey << 16) | (grey << 8) | grey;
-            pixels[y * width + x] = 0xff000000 | (0x00010101 * grey);
-          }
-        }
+    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+    bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
+    return bitmap;
+  }
 
-        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
-        Message message = Message.obtain(handler, R.id.encode_succeeded);
-        message.obj = bitmap;
-        message.sendToTarget();
-      } catch (WriterException e) {
-        Log.e(TAG, e.toString());
-        Message message = Message.obtain(handler, R.id.encode_failed);
-        message.sendToTarget();
-      } catch (IllegalArgumentException e) {
-        Log.e(TAG, e.toString());
-        Message message = Message.obtain(handler, R.id.encode_failed);
-        message.sendToTarget();
+  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;
   }
 }