From fc42aacc43436d9b1148988f90712a924ef0fa58 Mon Sep 17 00:00:00 2001 From: "srowen@gmail.com" Date: Fri, 29 Mar 2013 14:39:31 +0000 Subject: [PATCH] First cut at specifying hints by Intent from Lachezar git-svn-id: https://zxing.googlecode.com/svn/trunk@2599 59b500cc-1b3d-0410-9834-0bbf25fbcc57 --- .../zxing/client/android/CaptureActivity.java | 7 +- .../android/CaptureActivityHandler.java | 5 +- .../client/android/DecodeHintManager.java | 236 ++++++++++++++++++ .../zxing/client/android/DecodeThread.java | 6 + core/src/com/google/zxing/DecodeHintType.java | 48 +++- 5 files changed, 288 insertions(+), 14 deletions(-) create mode 100644 android/src/com/google/zxing/client/android/DecodeHintManager.java diff --git a/android/src/com/google/zxing/client/android/CaptureActivity.java b/android/src/com/google/zxing/client/android/CaptureActivity.java index 033e4a78f..9149b6642 100755 --- a/android/src/com/google/zxing/client/android/CaptureActivity.java +++ b/android/src/com/google/zxing/client/android/CaptureActivity.java @@ -17,6 +17,7 @@ package com.google.zxing.client.android; import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; import com.google.zxing.Result; import com.google.zxing.ResultMetadataType; import com.google.zxing.ResultPoint; @@ -111,6 +112,7 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal private String sourceUrl; private ScanFromWebPageManager scanFromWebPageManager; private Collection decodeFormats; + private Map decodeHints; private String characterSet; private HistoryManager historyManager; private InactivityTimer inactivityTimer; @@ -207,6 +209,7 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal // Scan the formats the intent requested, and return the result to the calling activity. source = IntentSource.NATIVE_APP_INTENT; decodeFormats = DecodeFormatManager.parseDecodeFormats(intent); + decodeHints = DecodeHintManager.parseDecodeHints(intent); if (intent.hasExtra(Intents.Scan.WIDTH) && intent.hasExtra(Intents.Scan.HEIGHT)) { int width = intent.getIntExtra(Intents.Scan.WIDTH, 0); @@ -239,6 +242,8 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal Uri inputUri = Uri.parse(dataString); scanFromWebPageManager = new ScanFromWebPageManager(inputUri); decodeFormats = DecodeFormatManager.parseDecodeFormats(inputUri); + // Allow a sub-set of the hints to be specified by the caller. + decodeHints = DecodeHintManager.parseDecodeHints(inputUri); } @@ -710,7 +715,7 @@ public final class CaptureActivity extends Activity implements SurfaceHolder.Cal cameraManager.openDriver(surfaceHolder); // Creating the handler starts the preview, which can also throw a RuntimeException. if (handler == null) { - handler = new CaptureActivityHandler(this, decodeFormats, characterSet, cameraManager); + handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager); } decodeOrStoreSavedBitmap(null, null); } catch (IOException ioe) { diff --git a/android/src/com/google/zxing/client/android/CaptureActivityHandler.java b/android/src/com/google/zxing/client/android/CaptureActivityHandler.java index 779d1abcc..1260f428d 100755 --- a/android/src/com/google/zxing/client/android/CaptureActivityHandler.java +++ b/android/src/com/google/zxing/client/android/CaptureActivityHandler.java @@ -22,6 +22,7 @@ import android.content.pm.ResolveInfo; import android.graphics.BitmapFactory; import android.provider.Browser; import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; import com.google.zxing.Result; import com.google.zxing.client.android.camera.CameraManager; @@ -35,6 +36,7 @@ import android.os.Message; import android.util.Log; import java.util.Collection; +import java.util.Map; /** * This class handles all the messaging which comprises the state machine for capture. @@ -58,10 +60,11 @@ public final class CaptureActivityHandler extends Handler { CaptureActivityHandler(CaptureActivity activity, Collection decodeFormats, + Map baseHints, String characterSet, CameraManager cameraManager) { this.activity = activity; - decodeThread = new DecodeThread(activity, decodeFormats, characterSet, + decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet, new ViewfinderResultPointCallback(activity.getViewfinderView())); decodeThread.start(); state = State.SUCCESS; diff --git a/android/src/com/google/zxing/client/android/DecodeHintManager.java b/android/src/com/google/zxing/client/android/DecodeHintManager.java new file mode 100644 index 000000000..fd9f9ecb0 --- /dev/null +++ b/android/src/com/google/zxing/client/android/DecodeHintManager.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.android; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.zxing.DecodeHintType; + +/** + * @author Lachezar Dobrev + */ +final class DecodeHintManager { + + private static final String TAG = DecodeHintManager.class.getSimpleName(); + + // This pattern is used in decoding integer arrays. + private static final Pattern COMMA = Pattern.compile(","); + + private DecodeHintManager() {} + + /** + *

Split a query string into a list of name-value pairs.

+ * + *

This is an alternative to the {@link Uri#getQueryParameterNames()} and + * {@link Uri#getQueryParameters(String)}, which are quirky and not suitable + * for exist-only Uri parameters.

+ * + *

This method ignores multiple parameters with the same name and returns the + * first one only. This is technically incorrect, but should be acceptable due + * to the method of processing Hints: no multiple values for a hint.

+ * + * @param query query to split + * @return name-value pairs + */ + private static Map splitQuery(String query) { + Map map = new HashMap(); + int pos = 0; + while (pos < query.length()) { + if (query.charAt(pos) == '&') { + // Skip consecutive ampersand separators. + pos ++; + continue; + } + int amp = query.indexOf('&', pos); + int equ = query.indexOf('=', pos); + if (amp < 0) { + // This is the last element in the query, no more ampersand elements. + String name; + String text; + if (equ < 0) { + // No equal sign + name = query.substring(pos); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + text = ""; + } else { + // Split name and text. + name = query.substring(pos, equ); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + text = query.substring(equ + 1); + text = text.replace('+', ' '); // Preemptively decode + + text = Uri.decode(text); + } + if (!map.containsKey(name)) { + map.put(name, text); + } + break; + } + if (equ < 0 || equ > amp) { + // No equal sign until the &: this is a simple parameter with no value. + String name = query.substring(pos, amp); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + if (!map.containsKey(name)) { + map.put(name, ""); + } + pos = amp + 1; + continue; + } + String name = query.substring(pos, equ); + name = name.replace('+', ' '); // Preemptively decode + + name = Uri.decode(name); + String text = query.substring(equ+1, amp); + text = text.replace('+', ' '); // Preemptively decode + + text = Uri.decode(text); + if (!map.containsKey(name)) { + map.put(name, text); + } + pos = amp + 1; + } + return map; + } + + static Map parseDecodeHints(Uri inputUri) { + String query = inputUri.getEncodedQuery(); + if (query == null || query.length() == 0) { + return null; + } + + // Extract parameters + Map parameters = splitQuery(query); + + Map hints = new EnumMap(DecodeHintType.class); + + for (DecodeHintType hintType: DecodeHintType.values()) { + + if (hintType == DecodeHintType.CHARACTER_SET || + hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK || + hintType == DecodeHintType.POSSIBLE_FORMATS) { + continue; // This hint is specified in another way + } + + String parameterName = hintType.name(); + String parameterText = parameters.get(parameterName); + if (parameterText == null) { + continue; + } + if (hintType.getValueType().equals(Object.class)) { + // This is an unspecified type of hint content. Use the value as is. + // TODO: Can we make a different assumption on this? + hints.put(hintType, parameterText); + continue; + } + if (hintType.getValueType().equals(Void.class)) { + // Void hints are just flags: use the constant specified by DecodeHintType + hints.put(hintType, Boolean.TRUE); + continue; + } + if (hintType.getValueType().equals(String.class)) { + // A string hint: use the decoded value. + hints.put(hintType, parameterText); + continue; + } + if (hintType.getValueType().equals(Boolean.class)) { + // A boolean hint: a few values for false, everything else is true. + // An empty parameter is simply a flag-style parameter, assuming true + if (parameterText.length() == 0) { + hints.put(hintType, Boolean.TRUE); + } else if ("0".equals(parameterText) || + "false".equalsIgnoreCase(parameterText) || + "no".equalsIgnoreCase(parameterText)) { + hints.put(hintType, Boolean.FALSE); + } else { + hints.put(hintType, Boolean.TRUE); + } + + continue; + } + if (hintType.getValueType().equals(int[].class)) { + // An integer array. Used to specify valid lengths. + // Strip a trailing comma as in Java style array initialisers. + if (parameterText.length() > 0 && parameterText.charAt(parameterText.length() - 1) == ',') { + parameterText = parameterText.substring(0, parameterText.length() - 1); + } + String[] values = COMMA.split(parameterText); + int[] array = new int[values.length]; + for (int i = 0; i < values.length; i++) { + try { + array[i] = Integer.parseInt(values[i]); + } catch (NumberFormatException ignored) { + Log.w(TAG, "Skipping array of integers hint " + hintType + " due to invalid numeric value: '" + values[i] + '\''); + array = null; + break; + } + } + if (array != null) { + hints.put(hintType, array); + } + continue; + } + Log.w(TAG, "Unsupported hint type '" + hintType + "' of type " + hintType.getValueType()); + } + + Log.i(TAG, "Hints from the URI: " + hints); + return hints; + } + + static Map parseDecodeHints(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras == null || extras.isEmpty()) { + return null; + } + Map hints = new EnumMap(DecodeHintType.class); + + for (DecodeHintType hintType: DecodeHintType.values()) { + + if (hintType == DecodeHintType.CHARACTER_SET || + hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK || + hintType == DecodeHintType.POSSIBLE_FORMATS) { + continue; // This hint is specified in another way + } + + String hintName = hintType.name(); + if (extras.containsKey(hintName)) { + if (hintType.getValueType().equals(Void.class)) { + // Void hints are just flags: use the constant specified by the DecodeHintType + hints.put(hintType, Boolean.TRUE); + } else { + Object hintData = extras.get(hintName); + if (hintType.getValueType().isInstance(hintData)) { + hints.put(hintType, hintData); + } else { + Log.w(TAG, "Ignoring hint " + hintType + " because it is not assignable from " + hintData); + } + } + } + } + + Log.i(TAG, "Hints from the Intent: " + hints); + return hints; + } + +} diff --git a/android/src/com/google/zxing/client/android/DecodeThread.java b/android/src/com/google/zxing/client/android/DecodeThread.java index baf60a01e..99f3eff30 100755 --- a/android/src/com/google/zxing/client/android/DecodeThread.java +++ b/android/src/com/google/zxing/client/android/DecodeThread.java @@ -24,6 +24,7 @@ import android.content.SharedPreferences; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; +import android.util.Log; import java.util.Collection; import java.util.EnumMap; @@ -48,6 +49,7 @@ final class DecodeThread extends Thread { DecodeThread(CaptureActivity activity, Collection decodeFormats, + Map baseHints, String characterSet, ResultPointCallback resultPointCallback) { @@ -55,6 +57,9 @@ final class DecodeThread extends Thread { handlerInitLatch = new CountDownLatch(1); hints = new EnumMap(DecodeHintType.class); + if (baseHints != null) { + hints.putAll(baseHints); + } // The prefs can't change while the thread is running, so pick them up once here. if (decodeFormats == null || decodeFormats.isEmpty()) { @@ -76,6 +81,7 @@ final class DecodeThread extends Thread { hints.put(DecodeHintType.CHARACTER_SET, characterSet); } hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback); + Log.i("DecodeThread", "Hints: " + hints); } Handler getHandler() { diff --git a/core/src/com/google/zxing/DecodeHintType.java b/core/src/com/google/zxing/DecodeHintType.java index 2e48c737b..a70453dfc 100644 --- a/core/src/com/google/zxing/DecodeHintType.java +++ b/core/src/com/google/zxing/DecodeHintType.java @@ -16,6 +16,8 @@ package com.google.zxing; +import java.util.List; + /** * Encapsulates a type of hint that a caller may pass to a barcode reader to help it * more quickly or accurately decode it. It is up to implementations to decide what, @@ -30,52 +32,74 @@ public enum DecodeHintType { /** * Unspecified, application-specific hint. Maps to an unspecified {@link Object}. */ - OTHER, + OTHER(Object.class), /** * Image is a pure monochrome image of a barcode. Doesn't matter what it maps to; * use {@link Boolean#TRUE}. */ - PURE_BARCODE, + PURE_BARCODE(Void.class), /** * Image is known to be of one of a few possible formats. - * Maps to a {@link java.util.List} of {@link BarcodeFormat}s. + * Maps to a {@link List} of {@link BarcodeFormat}s. */ - POSSIBLE_FORMATS, + POSSIBLE_FORMATS(List.class), /** * Spend more time to try to find a barcode; optimize for accuracy, not speed. * Doesn't matter what it maps to; use {@link Boolean#TRUE}. */ - TRY_HARDER, + TRY_HARDER(Void.class), /** * Specifies what character encoding to use when decoding, where applicable (type String) */ - CHARACTER_SET, + CHARACTER_SET(String.class), /** - * Allowed lengths of encoded data -- reject anything else. Maps to an int[]. + * Allowed lengths of encoded data -- reject anything else. Maps to an {@code int[]}. */ - ALLOWED_LENGTHS, + ALLOWED_LENGTHS(int[].class), /** - * Assume Code 39 codes employ a check digit. Maps to {@link Boolean}. + * Assume Code 39 codes employ a check digit. Doesn't matter what it maps to; + * use {@link Boolean#TRUE}. */ - ASSUME_CODE_39_CHECK_DIGIT, + ASSUME_CODE_39_CHECK_DIGIT(Void.class), /** * Assume the barcode is being processed as a GS1 barcode, and modify behavior as needed. * For example this affects FNC1 handling for Code 128 (aka GS1-128). Doesn't matter what it maps to; * use {@link Boolean#TRUE}. */ - ASSUME_GS1, + ASSUME_GS1(Void.class), /** * The caller needs to be notified via callback when a possible {@link ResultPoint} * is found. Maps to a {@link ResultPointCallback}. */ - NEED_RESULT_POINT_CALLBACK, + NEED_RESULT_POINT_CALLBACK(ResultPointCallback.class), + + // End of enumeration values. + ; + + /** + * Data type the hint is expecting. + * Among the possible values the {@link Void} stands out as being used for + * hints that do not expect a value to be supplied (flag hints). Such hints + * will possibly have their value ignored, or replaced by a + * {@link Boolean#TRUE}. Hint suppliers should probably use + * {@link Boolean#TRUE} as directed by the actual hint documentation. + */ + private final Class valueType; + + DecodeHintType(Class valueType) { + this.valueType = valueType; + } + + public Class getValueType() { + return valueType; + } }