- Prevented secure URIs from being added to History or copied to the clipboard.

- Made the History display parsed entries instead of raw contents.
- Some minor cleanup and comments.

git-svn-id: https://zxing.googlecode.com/svn/trunk@1813 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
dswitkin@google.com 2011-06-07 22:37:13 +00:00
parent 886e3a3e02
commit e04e3ef2c1
8 changed files with 112 additions and 85 deletions

View file

@ -20,8 +20,8 @@ version to be published. The next versionCode will be 7, regardless of whether t
versionName is 2.31, 2.4, or 3.0. --> versionName is 2.31, 2.4, or 3.0. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.zxing.client.android" package="com.google.zxing.client.android"
android:versionName="3.6" android:versionName="3.61 beta 1"
android:versionCode="70" android:versionCode="71"
android:installLocation="auto"> android:installLocation="auto">
<!-- We require Cupcake (Android 1.5) or later, but are really targeting Donut. --> <!-- We require Cupcake (Android 1.5) or later, but are really targeting Donut. -->
<uses-sdk android:minSdkVersion="3" <uses-sdk android:minSdkVersion="3"

View file

@ -4,6 +4,11 @@
<link rel="stylesheet" href="style.css" type="text/css"/> <link rel="stylesheet" href="style.css" type="text/css"/>
</head> </head>
<body> <body>
<p><b>New in version 3.61:</b></p>
<ul>
<li>Fixed a couple of crashes.</li>
<li>Made items in the History easier to read.</li>
</ul>
<p><b>New in version 3.6:</b></p> <p><b>New in version 3.6:</b></p>
<ul> <ul>
<li>Added support for the Motorola Xoom and other tablets.</li> <li>Added support for the Motorola Xoom and other tablets.</li>

View file

@ -364,23 +364,25 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal
public void handleDecode(Result rawResult, Bitmap barcode) { public void handleDecode(Result rawResult, Bitmap barcode) {
inactivityTimer.onActivity(); inactivityTimer.onActivity();
lastResult = rawResult; lastResult = rawResult;
historyManager.addHistoryItem(rawResult); ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);
historyManager.addHistoryItem(rawResult, resultHandler);
if (barcode == null) { if (barcode == null) {
// This is from history -- no saved barcode // This is from history -- no saved barcode
handleDecodeInternally(rawResult, null); handleDecodeInternally(rawResult, resultHandler, null);
} else { } else {
beepManager.playBeepSoundAndVibrate(); beepManager.playBeepSoundAndVibrate();
drawResultPoints(barcode, rawResult); drawResultPoints(barcode, rawResult);
switch (source) { switch (source) {
case NATIVE_APP_INTENT: case NATIVE_APP_INTENT:
case PRODUCT_SEARCH_LINK: case PRODUCT_SEARCH_LINK:
handleDecodeExternally(rawResult, barcode); handleDecodeExternally(rawResult, resultHandler, barcode);
break; break;
case ZXING_LINK: case ZXING_LINK:
if (returnUrlTemplate == null){ if (returnUrlTemplate == null){
handleDecodeInternally(rawResult, barcode); handleDecodeInternally(rawResult, resultHandler, barcode);
} else { } else {
handleDecodeExternally(rawResult, barcode); handleDecodeExternally(rawResult, resultHandler, barcode);
} }
break; break;
case NONE: case NONE:
@ -393,7 +395,7 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal
} }
resetStatusView(); resetStatusView();
} else { } else {
handleDecodeInternally(rawResult, barcode); handleDecodeInternally(rawResult, resultHandler, barcode);
} }
break; break;
} }
@ -441,7 +443,7 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal
} }
// Put up our own UI for how to handle the decoded contents. // Put up our own UI for how to handle the decoded contents.
private void handleDecodeInternally(Result rawResult, Bitmap barcode) { private void handleDecodeInternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) {
statusView.setVisibility(View.GONE); statusView.setVisibility(View.GONE);
viewfinderView.setVisibility(View.GONE); viewfinderView.setVisibility(View.GONE);
resultView.setVisibility(View.VISIBLE); resultView.setVisibility(View.VISIBLE);
@ -457,7 +459,6 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal
TextView formatTextView = (TextView) findViewById(R.id.format_text_view); TextView formatTextView = (TextView) findViewById(R.id.format_text_view);
formatTextView.setText(rawResult.getBarcodeFormat().toString()); formatTextView.setText(rawResult.getBarcodeFormat().toString());
ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);
TextView typeTextView = (TextView) findViewById(R.id.type_text_view); TextView typeTextView = (TextView) findViewById(R.id.type_text_view);
typeTextView.setText(resultHandler.getType().toString()); typeTextView.setText(resultHandler.getType().toString());
@ -518,23 +519,22 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal
} }
} }
if (copyToClipboard) { if (copyToClipboard && !resultHandler.areContentsSecure()) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(displayContents); clipboard.setText(displayContents);
} }
} }
// Briefly show the contents of the barcode, then handle the result outside Barcode Scanner. // Briefly show the contents of the barcode, then handle the result outside Barcode Scanner.
private void handleDecodeExternally(Result rawResult, Bitmap barcode) { private void handleDecodeExternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) {
viewfinderView.drawResultBitmap(barcode); viewfinderView.drawResultBitmap(barcode);
// Since this message will only be shown for a second, just tell the user what kind of // Since this message will only be shown for a second, just tell the user what kind of
// barcode was found (e.g. contact info) rather than the full contents, which they won't // barcode was found (e.g. contact info) rather than the full contents, which they won't
// have time to read. // have time to read.
ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);
statusView.setText(getString(resultHandler.getDisplayTitle())); statusView.setText(getString(resultHandler.getDisplayTitle()));
if (copyToClipboard) { if (copyToClipboard && !resultHandler.areContentsSecure()) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(resultHandler.getDisplayContents()); clipboard.setText(resultHandler.getDisplayContents());
} }

View file

@ -16,38 +16,39 @@
package com.google.zxing.client.android.history; package com.google.zxing.client.android.history;
import java.util.List; import com.google.zxing.Result;
import com.google.zxing.client.android.CaptureActivity;
import com.google.zxing.client.android.R;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Message; import android.os.Message;
import com.google.zxing.Result;
import com.google.zxing.client.android.CaptureActivity; import java.util.List;
import com.google.zxing.client.android.R;
final class HistoryClickListener implements DialogInterface.OnClickListener { final class HistoryClickListener implements DialogInterface.OnClickListener {
private final HistoryManager historyManager; private final HistoryManager historyManager;
private final CaptureActivity activity; private final CaptureActivity activity;
private final String[] dialogItems;
private final List<Result> items; private final List<Result> items;
HistoryClickListener(HistoryManager historyManager, /**
CaptureActivity activity, * Handles clicks in the History dialog.
String[] dialogItems, *
List<Result> items) { * @author dswitkin@google.com (Daniel Switkin)
* @author Sean Owen
*/
HistoryClickListener(HistoryManager historyManager, CaptureActivity activity, List<Result> items) {
this.historyManager = historyManager; this.historyManager = historyManager;
this.activity = activity; this.activity = activity;
this.dialogItems = dialogItems;
this.items = items; this.items = items;
} }
public void onClick(DialogInterface dialogInterface, int i) { public void onClick(DialogInterface dialogInterface, int i) {
if (i == dialogItems.length - 1) { if (i == items.size()) {
historyManager.clearHistory(); // Share history.
} else if (i == dialogItems.length - 2) {
CharSequence history = historyManager.buildHistory(); CharSequence history = historyManager.buildHistory();
Uri historyFile = HistoryManager.saveHistory(history.toString()); Uri historyFile = HistoryManager.saveHistory(history.toString());
if (historyFile == null) { if (historyFile == null) {
@ -65,7 +66,11 @@ final class HistoryClickListener implements DialogInterface.OnClickListener {
intent.putExtra(Intent.EXTRA_STREAM, historyFile); intent.putExtra(Intent.EXTRA_STREAM, historyFile);
intent.setType("text/csv"); intent.setType("text/csv");
activity.startActivity(intent); activity.startActivity(intent);
} else if (i == items.size() + 1) {
// Clear history.
historyManager.clearHistory();
} else { } else {
// Display a single history entry.
Result result = items.get(i); Result result = items.get(i);
Message message = Message.obtain(activity.getHandler(), R.id.decode_succeeded, result); Message message = Message.obtain(activity.getHandler(), R.id.decode_succeeded, result);
message.sendToTarget(); message.sendToTarget();

View file

@ -16,17 +16,27 @@
package com.google.zxing.client.android.history; package com.google.zxing.client.android.history;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.google.zxing.client.android.CaptureActivity;
import com.google.zxing.client.android.Intents;
import com.google.zxing.client.android.PreferencesActivity;
import com.google.zxing.client.android.R;
import com.google.zxing.client.android.result.ResultHandler;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -34,19 +44,9 @@ import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Collections; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.android.Intents;
import com.google.zxing.client.android.PreferencesActivity;
import com.google.zxing.client.android.R;
import com.google.zxing.client.android.CaptureActivity;
import com.google.zxing.Result;
/** /**
* <p>Manages functionality related to scan history.</p> * <p>Manages functionality related to scan history.</p>
@ -54,13 +54,13 @@ import com.google.zxing.Result;
* @author Sean Owen * @author Sean Owen
*/ */
public final class HistoryManager { public final class HistoryManager {
private static final String TAG = HistoryManager.class.getSimpleName(); private static final String TAG = HistoryManager.class.getSimpleName();
private static final int MAX_ITEMS = 500; private static final int MAX_ITEMS = 500;
//private static final String[] TEXT_COL_PROJECTION = { DBHelper.TEXT_COL };
private static final String[] GET_ITEM_COL_PROJECTION = { private static final String[] GET_ITEM_COL_PROJECTION = {
DBHelper.TEXT_COL, DBHelper.TEXT_COL,
DBHelper.DISPLAY_COL,
DBHelper.FORMAT_COL, DBHelper.FORMAT_COL,
DBHelper.TIMESTAMP_COL, DBHelper.TIMESTAMP_COL,
}; };
@ -79,60 +79,53 @@ public final class HistoryManager {
this.activity = activity; this.activity = activity;
} }
List<Result> getHistoryItems() { public AlertDialog buildAlert() {
SQLiteOpenHelper helper = new DBHelper(activity); SQLiteOpenHelper helper = new DBHelper(activity);
List<Result> items = new ArrayList<Result>(); List<Result> items = new ArrayList<Result>();
SQLiteDatabase db; List<String> dialogItems = new ArrayList<String>();
try { SQLiteDatabase db = null;
db = helper.getWritableDatabase();
} catch (SQLiteException sqle) {
Log.w(TAG, "Error while opening database", sqle);
return Collections.emptyList();
}
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = db.query(DBHelper.TABLE_NAME, db = helper.getWritableDatabase();
GET_ITEM_COL_PROJECTION, cursor = db.query(DBHelper.TABLE_NAME, GET_ITEM_COL_PROJECTION, null, null, null, null,
null, null, null, null,
DBHelper.TIMESTAMP_COL + " DESC"); DBHelper.TIMESTAMP_COL + " DESC");
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
Result result = new Result(cursor.getString(0), Result result = new Result(cursor.getString(0), null, null,
null, BarcodeFormat.valueOf(cursor.getString(2)), cursor.getLong(3));
null,
BarcodeFormat.valueOf(cursor.getString(1)),
cursor.getLong(2));
items.add(result); items.add(result);
String display = cursor.getString(1);
if (display == null || display.length() == 0) {
display = result.getText();
} }
dialogItems.add(display);
}
} catch (SQLiteException sqle) {
Log.w(TAG, "Error while opening database", sqle);
} finally { } finally {
if (cursor != null) { if (cursor != null) {
cursor.close(); cursor.close();
} }
if (db != null) {
db.close(); db.close();
} }
return items;
} }
public AlertDialog buildAlert() {
List<Result> items = getHistoryItems();
int size = items.size();
String[] dialogItems = new String[size + 2];
for (int i = 0; i < size; i++) {
dialogItems[i] = items.get(i).getText();
}
Resources res = activity.getResources(); Resources res = activity.getResources();
dialogItems[dialogItems.length - 2] = res.getString(R.string.history_send); dialogItems.add(res.getString(R.string.history_send));
dialogItems[dialogItems.length - 1] = res.getString(R.string.history_clear_text); dialogItems.add(res.getString(R.string.history_clear_text));
DialogInterface.OnClickListener clickListener = new HistoryClickListener(this, activity, DialogInterface.OnClickListener clickListener = new HistoryClickListener(this, activity, items);
dialogItems, items);
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.history_title); builder.setTitle(R.string.history_title);
builder.setItems(dialogItems, clickListener); builder.setItems(dialogItems.toArray(new String[dialogItems.size()]), clickListener);
return builder.create(); return builder.create();
} }
public void addHistoryItem(Result result) { public void addHistoryItem(Result result, ResultHandler handler) {
if (!activity.getIntent().getBooleanExtra(Intents.Scan.SAVE_HISTORY, true)) { // Do not save this item to the history if the preference is turned off, or the contents are
return; // Do not save this item to the history. // considered secure.
if (!activity.getIntent().getBooleanExtra(Intents.Scan.SAVE_HISTORY, true) ||
handler.areContentsSecure()) {
return;
} }
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
@ -149,13 +142,11 @@ public final class HistoryManager {
return; return;
} }
try { try {
// Insert // Insert the new entry into the DB.
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DBHelper.TEXT_COL, result.getText()); values.put(DBHelper.TEXT_COL, result.getText());
values.put(DBHelper.FORMAT_COL, result.getBarcodeFormat().toString()); values.put(DBHelper.FORMAT_COL, result.getBarcodeFormat().toString());
// It would be nice to use ParsedResult.getDisplayResult() instead, but we don't want to values.put(DBHelper.DISPLAY_COL, handler.getDisplayContents().toString());
// parse it twice, and it can be multiline which won't fit in the current UI.
values.put(DBHelper.DISPLAY_COL, result.getText());
values.put(DBHelper.TIMESTAMP_COL, System.currentTimeMillis()); values.put(DBHelper.TIMESTAMP_COL, System.currentTimeMillis());
db.insert(DBHelper.TABLE_NAME, DBHelper.TIMESTAMP_COL, values); db.insert(DBHelper.TABLE_NAME, DBHelper.TIMESTAMP_COL, values);
} finally { } finally {

View file

@ -143,6 +143,16 @@ public abstract class ResultHandler {
*/ */
public abstract void handleButtonPress(int index); public abstract void handleButtonPress(int index);
/**
* Some barcode contents are considered secure, and should not be saved to history, copied to
* the clipboard, or otherwise persisted.
*
* @return If true, do not create any permanent record of these contents.
*/
public boolean areContentsSecure() {
return false;
}
/** /**
* The Google Shopper button is special and is not handled by the abstract button methods above. * The Google Shopper button is special and is not handled by the abstract button methods above.
* *

View file

@ -28,6 +28,11 @@ import android.app.Activity;
* @author dswitkin@google.com (Daniel Switkin) * @author dswitkin@google.com (Daniel Switkin)
*/ */
public final class URIResultHandler extends ResultHandler { public final class URIResultHandler extends ResultHandler {
// URIs beginning with entries in this array will not be saved to history or copied to the
// clipboard for security.
private static final String[] SECURE_PROTOCOLS = new String[] {
"otpauth:"
};
private static final int[] buttons = { private static final int[] buttons = {
R.string.button_open_browser, R.string.button_open_browser,
@ -75,9 +80,20 @@ public final class URIResultHandler extends ResultHandler {
return R.string.result_uri; return R.string.result_uri;
} }
@Override
public boolean areContentsSecure() {
URIParsedResult uriResult = (URIParsedResult) getResult();
String uri = uriResult.getURI().toLowerCase();
for (String secure : SECURE_PROTOCOLS) {
if (uri.startsWith(secure)) {
return true;
}
}
return false;
}
private boolean isGoogleBooksURI() { private boolean isGoogleBooksURI() {
// FIXME(dswitkin): Should not hardcode Books URL. Also does not handle books.google.ca etc. // FIXME(dswitkin): Should not hardcode Books URL. Also does not handle books.google.ca etc.
return ((URIParsedResult) getResult()).getURI().startsWith("http://google.com/books?id="); return ((URIParsedResult) getResult()).getURI().startsWith("http://google.com/books?id=");
} }
} }

View file

@ -70,6 +70,7 @@ public abstract class ResultParser {
} else if ((result = URLTOResultParser.parse(theResult)) != null) { } else if ((result = URLTOResultParser.parse(theResult)) != null) {
return result; return result;
} else if ((result = URIResultParser.parse(theResult)) != null) { } else if ((result = URIResultParser.parse(theResult)) != null) {
// URI is a catch-all for protocol: contents that we don't handle explicitly above.
return result; return result;
} else if ((result = ISBNResultParser.parse(theResult)) != null) { } else if ((result = ISBNResultParser.parse(theResult)) != null) {
// We depend on ISBN parsing coming before UPC, as it is a subset. // We depend on ISBN parsing coming before UPC, as it is a subset.
@ -126,7 +127,6 @@ public abstract class ResultParser {
} }
private static String urlDecode(String escaped) { private static String urlDecode(String escaped) {
// No we can't use java.net.URLDecoder here. JavaME doesn't have it. // No we can't use java.net.URLDecoder here. JavaME doesn't have it.
if (escaped == null) { if (escaped == null) {
return null; return null;
@ -139,7 +139,7 @@ public abstract class ResultParser {
} }
int max = escapedArray.length; int max = escapedArray.length;
// final length is at most 2 less than original due to at least 1 unescaping // Final length is at most 2 less than original due to at least 1 unescaping.
StringBuffer unescaped = new StringBuffer(max - 2); StringBuffer unescaped = new StringBuffer(max - 2);
// Can append everything up to first escape character // Can append everything up to first escape character
unescaped.append(escapedArray, 0, first); unescaped.append(escapedArray, 0, first);
@ -152,14 +152,14 @@ public abstract class ResultParser {
unescaped.append(' '); unescaped.append(' ');
break; break;
case '%': case '%':
// Are there even two more chars? if not we will just copy the escaped sequence and be done // Are there even two more chars? If not we'll just copy the escaped sequence and be done.
if (i >= max - 2) { if (i >= max - 2) {
unescaped.append('%'); // append that % and move on unescaped.append('%'); // append that % and move on
} else { } else {
int firstDigitValue = parseHexDigit(escapedArray[++i]); int firstDigitValue = parseHexDigit(escapedArray[++i]);
int secondDigitValue = parseHexDigit(escapedArray[++i]); int secondDigitValue = parseHexDigit(escapedArray[++i]);
if (firstDigitValue < 0 || secondDigitValue < 0) { if (firstDigitValue < 0 || secondDigitValue < 0) {
// bad digit, just move on // Bad digit, just move on.
unescaped.append('%'); unescaped.append('%');
unescaped.append(escapedArray[i - 1]); unescaped.append(escapedArray[i - 1]);
unescaped.append(escapedArray[i]); unescaped.append(escapedArray[i]);