hints) {
+ multiFormatReader = new MultiFormatReader();
+ multiFormatReader.setHints(hints);
+ this.activity = activity;
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ if (!running) {
+ return;
+ }
+ switch (message.what) {
+ case R.id.decode:
+ decode((byte[]) message.obj, message.arg1, message.arg2);
+ break;
+ case R.id.quit:
+ running = false;
+ Looper.myLooper().quit();
+ break;
+ }
+ }
+
+ /**
+ * Decode the data within the viewfinder rectangle, and time how long it took. For efficiency,
+ * reuse the same reader objects from one decode to the next.
+ *
+ * @param data The YUV preview frame.
+ * @param width The width of the preview frame.
+ * @param height The height of the preview frame.
+ */
+ private void decode(byte[] data, int width, int height) {
+ long start = System.currentTimeMillis();
+ Result rawResult = null;
+ PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
+ if (source != null) {
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ try {
+ rawResult = multiFormatReader.decodeWithState(bitmap);
+ } catch (ReaderException re) {
+ // continue
+ } finally {
+ multiFormatReader.reset();
+ }
+ }
+
+ Handler handler = activity.getHandler();
+ if (rawResult != null) {
+ // Don't log the barcode contents for security.
+ long end = System.currentTimeMillis();
+ Log.d(TAG, "Found barcode in " + (end - start) + " ms");
+ if (handler != null) {
+ Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
+ Bundle bundle = new Bundle();
+ bundleThumbnail(source, bundle);
+ message.setData(bundle);
+ message.sendToTarget();
+ }
+ } else {
+ if (handler != null) {
+ Message message = Message.obtain(handler, R.id.decode_failed);
+ message.sendToTarget();
+ }
+ }
+ }
+
+ private static void bundleThumbnail(PlanarYUVLuminanceSource source, Bundle bundle) {
+ int[] pixels = source.renderThumbnail();
+ int width = source.getThumbnailWidth();
+ int height = source.getThumbnailHeight();
+ Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out);
+ bundle.putByteArray(DecodeThread.BARCODE_BITMAP, out.toByteArray());
+ bundle.putFloat(DecodeThread.BARCODE_SCALED_FACTOR, (float) width / source.getWidth());
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/DecodeHintManager.java b/app/src/main/java/com/google/zxing/client/android/DecodeHintManager.java
new file mode 100644
index 0000000..39305fe
--- /dev/null
+++ b/app/src/main/java/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.isEmpty()) {
+ 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.isEmpty()) {
+ 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.isEmpty() && 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/app/src/main/java/com/google/zxing/client/android/DecodeThread.java b/app/src/main/java/com/google/zxing/client/android/DecodeThread.java
new file mode 100644
index 0000000..fab4039
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/DecodeThread.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2008 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 com.google.zxing.BarcodeFormat;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.ResultPointCallback;
+
+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;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * This thread does all the heavy lifting of decoding the images.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class DecodeThread extends Thread {
+
+ public static final String BARCODE_BITMAP = "barcode_bitmap";
+ public static final String BARCODE_SCALED_FACTOR = "barcode_scaled_factor";
+
+ private final CaptureActivity activity;
+ private final Map hints;
+ private Handler handler;
+ private final CountDownLatch handlerInitLatch;
+
+ DecodeThread(CaptureActivity activity,
+ Collection decodeFormats,
+ Map baseHints,
+ String characterSet,
+ ResultPointCallback resultPointCallback) {
+
+ this.activity = activity;
+ 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()) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
+ decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_PRODUCT, true)) {
+ decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
+ }
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, true)) {
+ decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
+ }
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) {
+ decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
+ }
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) {
+ decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
+ }
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_AZTEC, false)) {
+ decodeFormats.addAll(DecodeFormatManager.AZTEC_FORMATS);
+ }
+ if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_PDF417, false)) {
+ decodeFormats.addAll(DecodeFormatManager.PDF417_FORMATS);
+ }
+ }
+ hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);
+
+ if (characterSet != null) {
+ hints.put(DecodeHintType.CHARACTER_SET, characterSet);
+ }
+ hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
+ Log.i("DecodeThread", "Hints: " + hints);
+ }
+
+ Handler getHandler() {
+ try {
+ handlerInitLatch.await();
+ } catch (InterruptedException ie) {
+ // continue?
+ }
+ return handler;
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ handler = new DecodeHandler(activity, hints);
+ handlerInitLatch.countDown();
+ Looper.loop();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/FinishListener.java b/app/src/main/java/com/google/zxing/client/android/FinishListener.java
new file mode 100644
index 0000000..5d59886
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/FinishListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.DialogInterface;
+
+/**
+ * Simple listener used to exit the app in a few cases.
+ *
+ * @author Sean Owen
+ */
+public final class FinishListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+
+ private final Activity activityToFinish;
+
+ public FinishListener(Activity activityToFinish) {
+ this.activityToFinish = activityToFinish;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ run();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ run();
+ }
+
+ private void run() {
+ activityToFinish.finish();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/HelpActivity.java b/app/src/main/java/com/google/zxing/client/android/HelpActivity.java
new file mode 100644
index 0000000..1f964bd
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/HelpActivity.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008 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 android.app.Activity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.webkit.WebView;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * An HTML-based help screen.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class HelpActivity extends Activity {
+
+ private static final String BASE_URL =
+ "file:///android_asset/html-" + LocaleManager.getTranslatedAssetLanguage() + '/';
+
+ private WebView webView;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.help);
+
+ webView = (WebView) findViewById(R.id.help_contents);
+
+ if (icicle == null) {
+ webView.loadUrl(BASE_URL + "index.html");
+ } else {
+ webView.restoreState(icicle);
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
+ webView.goBack();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ webView.saveState(icicle);
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/HttpHelper.java b/app/src/main/java/com/google/zxing/client/android/HttpHelper.java
new file mode 100644
index 0000000..5a1543d
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/HttpHelper.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2011 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 android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+
+/**
+ * Utility methods for retrieving content over HTTP using the more-supported {@code java.net} classes
+ * in Android.
+ */
+public final class HttpHelper {
+
+ private static final String TAG = HttpHelper.class.getSimpleName();
+
+ private static final Collection REDIRECTOR_DOMAINS = new HashSet<>(Arrays.asList(
+ "amzn.to", "bit.ly", "bitly.com", "fb.me", "goo.gl", "is.gd", "j.mp", "lnkd.in", "ow.ly",
+ "R.BEETAGG.COM", "r.beetagg.com", "SCN.BY", "su.pr", "t.co", "tinyurl.com", "tr.im"
+ ));
+
+ private HttpHelper() {
+ }
+
+ public enum ContentType {
+ /** HTML-like content type, including HTML, XHTML, etc. */
+ HTML,
+ /** JSON content */
+ JSON,
+ /** XML */
+ XML,
+ /** Plain text content */
+ TEXT,
+ }
+
+ /**
+ * Downloads the entire resource instead of part.
+ *
+ * @param uri URI to retrieve
+ * @param type expected text-like MIME type of that content
+ * @return content as a {@code String}
+ * @throws IOException if the content can't be retrieved because of a bad URI, network problem, etc.
+ * @see #downloadViaHttp(String, HttpHelper.ContentType, int)
+ */
+ public static CharSequence downloadViaHttp(String uri, ContentType type) throws IOException {
+ return downloadViaHttp(uri, type, Integer.MAX_VALUE);
+ }
+
+ /**
+ * @param uri URI to retrieve
+ * @param type expected text-like MIME type of that content
+ * @param maxChars approximate maximum characters to read from the source
+ * @return content as a {@code String}
+ * @throws IOException if the content can't be retrieved because of a bad URI, network problem, etc.
+ */
+ public static CharSequence downloadViaHttp(String uri, ContentType type, int maxChars) throws IOException {
+ String contentTypes;
+ switch (type) {
+ case HTML:
+ contentTypes = "application/xhtml+xml,text/html,text/*,*/*";
+ break;
+ case JSON:
+ contentTypes = "application/json,text/*,*/*";
+ break;
+ case XML:
+ contentTypes = "application/xml,text/*,*/*";
+ break;
+ case TEXT:
+ default:
+ contentTypes = "text/*,*/*";
+ }
+ return downloadViaHttp(uri, contentTypes, maxChars);
+ }
+
+ private static CharSequence downloadViaHttp(String uri, String contentTypes, int maxChars) throws IOException {
+ int redirects = 0;
+ while (redirects < 5) {
+ URL url = new URL(uri);
+ HttpURLConnection connection = safelyOpenConnection(url);
+ connection.setInstanceFollowRedirects(true); // Won't work HTTP -> HTTPS or vice versa
+ connection.setRequestProperty("Accept", contentTypes);
+ connection.setRequestProperty("Accept-Charset", "utf-8,*");
+ connection.setRequestProperty("User-Agent", "ZXing (Android)");
+ try {
+ int responseCode = safelyConnect(connection);
+ switch (responseCode) {
+ case HttpURLConnection.HTTP_OK:
+ return consume(connection, maxChars);
+ case HttpURLConnection.HTTP_MOVED_TEMP:
+ String location = connection.getHeaderField("Location");
+ if (location != null) {
+ uri = location;
+ redirects++;
+ continue;
+ }
+ throw new IOException("No Location");
+ default:
+ throw new IOException("Bad HTTP response: " + responseCode);
+ }
+ } finally {
+ connection.disconnect();
+ }
+ }
+ throw new IOException("Too many redirects");
+ }
+
+ private static String getEncoding(URLConnection connection) {
+ String contentTypeHeader = connection.getHeaderField("Content-Type");
+ if (contentTypeHeader != null) {
+ int charsetStart = contentTypeHeader.indexOf("charset=");
+ if (charsetStart >= 0) {
+ return contentTypeHeader.substring(charsetStart + "charset=".length());
+ }
+ }
+ return "UTF-8";
+ }
+
+ private static CharSequence consume(URLConnection connection, int maxChars) throws IOException {
+ String encoding = getEncoding(connection);
+ StringBuilder out = new StringBuilder();
+ Reader in = null;
+ try {
+ in = new InputStreamReader(connection.getInputStream(), encoding);
+ char[] buffer = new char[1024];
+ int charsRead;
+ while (out.length() < maxChars && (charsRead = in.read(buffer)) > 0) {
+ out.append(buffer, 0, charsRead);
+ }
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException | NullPointerException ioe) {
+ // continue
+ }
+ }
+ }
+ return out;
+ }
+
+ public static URI unredirect(URI uri) throws IOException {
+ if (!REDIRECTOR_DOMAINS.contains(uri.getHost())) {
+ return uri;
+ }
+ URL url = uri.toURL();
+ HttpURLConnection connection = safelyOpenConnection(url);
+ connection.setInstanceFollowRedirects(false);
+ connection.setDoInput(false);
+ connection.setRequestMethod("HEAD");
+ connection.setRequestProperty("User-Agent", "ZXing (Android)");
+ try {
+ int responseCode = safelyConnect(connection);
+ switch (responseCode) {
+ case HttpURLConnection.HTTP_MULT_CHOICE:
+ case HttpURLConnection.HTTP_MOVED_PERM:
+ case HttpURLConnection.HTTP_MOVED_TEMP:
+ case HttpURLConnection.HTTP_SEE_OTHER:
+ case 307: // No constant for 307 Temporary Redirect ?
+ String location = connection.getHeaderField("Location");
+ if (location != null) {
+ try {
+ return new URI(location);
+ } catch (URISyntaxException e) {
+ // nevermind
+ }
+ }
+ }
+ return uri;
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ private static HttpURLConnection safelyOpenConnection(URL url) throws IOException {
+ URLConnection conn;
+ try {
+ conn = url.openConnection();
+ } catch (NullPointerException npe) {
+ // Another strange bug in Android?
+ Log.w(TAG, "Bad URI? " + url);
+ throw new IOException(npe);
+ }
+ if (!(conn instanceof HttpURLConnection)) {
+ throw new IOException();
+ }
+ return (HttpURLConnection) conn;
+ }
+
+ private static int safelyConnect(HttpURLConnection connection) throws IOException {
+ try {
+ connection.connect();
+ } catch (NullPointerException | IllegalArgumentException | IndexOutOfBoundsException | SecurityException e) {
+ // this is an Android bug: http://code.google.com/p/android/issues/detail?id=16895
+ throw new IOException(e);
+ }
+ try {
+ return connection.getResponseCode();
+ } catch (NullPointerException | StringIndexOutOfBoundsException | IllegalArgumentException e) {
+ // this is maybe this Android bug: http://code.google.com/p/android/issues/detail?id=15554
+ throw new IOException(e);
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java b/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java
new file mode 100644
index 0000000..5e33d90
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/InactivityTimer.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.util.Log;
+
+/**
+ * Finishes an activity after a period of inactivity if the device is on battery power.
+ */
+final class InactivityTimer {
+
+ private static final String TAG = InactivityTimer.class.getSimpleName();
+
+ private static final long INACTIVITY_DELAY_MS = 5 * 60 * 1000L;
+
+ private final Activity activity;
+ private final BroadcastReceiver powerStatusReceiver;
+ private boolean registered;
+ private AsyncTask inactivityTask;
+
+ InactivityTimer(Activity activity) {
+ this.activity = activity;
+ powerStatusReceiver = new PowerStatusReceiver();
+ registered = false;
+ onActivity();
+ }
+
+ synchronized void onActivity() {
+ cancel();
+ inactivityTask = new InactivityAsyncTask();
+ inactivityTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public synchronized void onPause() {
+ cancel();
+ if (registered) {
+ activity.unregisterReceiver(powerStatusReceiver);
+ registered = false;
+ } else {
+ Log.w(TAG, "PowerStatusReceiver was never registered?");
+ }
+ }
+
+ public synchronized void onResume() {
+ if (registered) {
+ Log.w(TAG, "PowerStatusReceiver was already registered?");
+ } else {
+ activity.registerReceiver(powerStatusReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ registered = true;
+ }
+ onActivity();
+ }
+
+ private synchronized void cancel() {
+ AsyncTask,?,?> task = inactivityTask;
+ if (task != null) {
+ task.cancel(true);
+ inactivityTask = null;
+ }
+ }
+
+ void shutdown() {
+ cancel();
+ }
+
+ private final class PowerStatusReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent){
+ if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
+ // 0 indicates that we're on battery
+ boolean onBatteryNow = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) <= 0;
+ if (onBatteryNow) {
+ InactivityTimer.this.onActivity();
+ } else {
+ InactivityTimer.this.cancel();
+ }
+ }
+ }
+ }
+
+ private final class InactivityAsyncTask extends AsyncTask {
+ @Override
+ protected Object doInBackground(Object... objects) {
+ try {
+ Thread.sleep(INACTIVITY_DELAY_MS);
+ Log.i(TAG, "Finishing activity due to inactivity");
+ activity.finish();
+ } catch (InterruptedException e) {
+ // continue without killing
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/IntentSource.java b/app/src/main/java/com/google/zxing/client/android/IntentSource.java
new file mode 100644
index 0000000..3222db6
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/IntentSource.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 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;
+
+enum IntentSource {
+
+ NATIVE_APP_INTENT,
+ PRODUCT_SEARCH_LINK,
+ ZXING_LINK,
+ NONE
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/Intents.java b/app/src/main/java/com/google/zxing/client/android/Intents.java
new file mode 100644
index 0000000..6e59e80
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/Intents.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2008 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;
+
+/**
+ * This class provides the constants to use when sending an Intent to Barcode Scanner.
+ * These strings are effectively API and cannot be changed.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class Intents {
+ private Intents() {
+ }
+
+ public static final class Scan {
+ /**
+ * Send this intent to open the Barcodes app in scanning mode, find a barcode, and return
+ * the results.
+ */
+ public static final String ACTION = "com.google.zxing.client.android.SCAN";
+
+ /**
+ * By default, sending this will decode all barcodes that we understand. However it
+ * may be useful to limit scanning to certain formats. Use
+ * {@link android.content.Intent#putExtra(String, String)} with one of the values below.
+ *
+ * Setting this is effectively shorthand for setting explicit formats with {@link #FORMATS}.
+ * It is overridden by that setting.
+ */
+ public static final String MODE = "SCAN_MODE";
+
+ /**
+ * Decode only UPC and EAN barcodes. This is the right choice for shopping apps which get
+ * prices, reviews, etc. for products.
+ */
+ public static final String PRODUCT_MODE = "PRODUCT_MODE";
+
+ /**
+ * Decode only 1D barcodes.
+ */
+ public static final String ONE_D_MODE = "ONE_D_MODE";
+
+ /**
+ * Decode only QR codes.
+ */
+ public static final String QR_CODE_MODE = "QR_CODE_MODE";
+
+ /**
+ * Decode only Data Matrix codes.
+ */
+ public static final String DATA_MATRIX_MODE = "DATA_MATRIX_MODE";
+
+ /**
+ * Decode only Aztec.
+ */
+ public static final String AZTEC_MODE = "AZTEC_MODE";
+
+ /**
+ * Decode only PDF417.
+ */
+ public static final String PDF417_MODE = "PDF417_MODE";
+
+ /**
+ * Comma-separated list of formats to scan for. The values must match the names of
+ * {@link com.google.zxing.BarcodeFormat}s, e.g. {@link com.google.zxing.BarcodeFormat#EAN_13}.
+ * Example: "EAN_13,EAN_8,QR_CODE". This overrides {@link #MODE}.
+ */
+ public static final String FORMATS = "SCAN_FORMATS";
+
+ /**
+ * Optional parameter to specify the id of the camera from which to recognize barcodes.
+ * Overrides the default camera that would otherwise would have been selected.
+ * If provided, should be an int.
+ */
+ public static final String CAMERA_ID = "SCAN_CAMERA_ID";
+
+ /**
+ * @see com.google.zxing.DecodeHintType#CHARACTER_SET
+ */
+ public static final String CHARACTER_SET = "CHARACTER_SET";
+
+ /**
+ * Optional parameters to specify the width and height of the scanning rectangle in pixels.
+ * The app will try to honor these, but will clamp them to the size of the preview frame.
+ * You should specify both or neither, and pass the size as an int.
+ */
+ public static final String WIDTH = "SCAN_WIDTH";
+ public static final String HEIGHT = "SCAN_HEIGHT";
+
+ /**
+ * Desired duration in milliseconds for which to pause after a successful scan before
+ * returning to the calling intent. Specified as a long, not an integer!
+ * For example: 1000L, not 1000.
+ */
+ public static final String RESULT_DISPLAY_DURATION_MS = "RESULT_DISPLAY_DURATION_MS";
+
+ /**
+ * Prompt to show on-screen when scanning by intent. Specified as a {@link String}.
+ */
+ public static final String PROMPT_MESSAGE = "PROMPT_MESSAGE";
+
+ /**
+ * If a barcode is found, Barcodes returns {@link android.app.Activity#RESULT_OK} to
+ * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)}
+ * of the app which requested the scan via
+ * {@link android.app.Activity#startActivityForResult(android.content.Intent, int)}
+ * The barcodes contents can be retrieved with
+ * {@link android.content.Intent#getStringExtra(String)}.
+ * If the user presses Back, the result code will be {@link android.app.Activity#RESULT_CANCELED}.
+ */
+ public static final String RESULT = "SCAN_RESULT";
+
+ /**
+ * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_FORMAT}
+ * to determine which barcode format was found.
+ * See {@link com.google.zxing.BarcodeFormat} for possible values.
+ */
+ public static final String RESULT_FORMAT = "SCAN_RESULT_FORMAT";
+
+ /**
+ * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_UPC_EAN_EXTENSION}
+ * to return the content of any UPC extension barcode that was also found. Only applicable
+ * to {@link com.google.zxing.BarcodeFormat#UPC_A} and {@link com.google.zxing.BarcodeFormat#EAN_13}
+ * formats.
+ */
+ public static final String RESULT_UPC_EAN_EXTENSION = "SCAN_RESULT_UPC_EAN_EXTENSION";
+
+ /**
+ * Call {@link android.content.Intent#getByteArrayExtra(String)} with {@link #RESULT_BYTES}
+ * to get a {@code byte[]} of raw bytes in the barcode, if available.
+ */
+ public static final String RESULT_BYTES = "SCAN_RESULT_BYTES";
+
+ /**
+ * Key for the value of {@link com.google.zxing.ResultMetadataType#ORIENTATION}, if available.
+ * Call {@link android.content.Intent#getIntArrayExtra(String)} with {@link #RESULT_ORIENTATION}.
+ */
+ public static final String RESULT_ORIENTATION = "SCAN_RESULT_ORIENTATION";
+
+ /**
+ * Key for the value of {@link com.google.zxing.ResultMetadataType#ERROR_CORRECTION_LEVEL}, if available.
+ * Call {@link android.content.Intent#getStringExtra(String)} with {@link #RESULT_ERROR_CORRECTION_LEVEL}.
+ */
+ public static final String RESULT_ERROR_CORRECTION_LEVEL = "SCAN_RESULT_ERROR_CORRECTION_LEVEL";
+
+ /**
+ * Prefix for keys that map to the values of {@link com.google.zxing.ResultMetadataType#BYTE_SEGMENTS},
+ * if available. The actual values will be set under a series of keys formed by adding 0, 1, 2, ...
+ * to this prefix. So the first byte segment is under key "SCAN_RESULT_BYTE_SEGMENTS_0" for example.
+ * Call {@link android.content.Intent#getByteArrayExtra(String)} with these keys.
+ */
+ public static final String RESULT_BYTE_SEGMENTS_PREFIX = "SCAN_RESULT_BYTE_SEGMENTS_";
+
+ /**
+ * Setting this to false will not save scanned codes in the history. Specified as a {@code boolean}.
+ */
+ public static final String SAVE_HISTORY = "SAVE_HISTORY";
+
+ private Scan() {
+ }
+ }
+
+ public static final class History {
+
+ public static final String ITEM_NUMBER = "ITEM_NUMBER";
+
+ private History() {
+ }
+ }
+
+ public static final class Encode {
+ /**
+ * Send this intent to encode a piece of data as a QR code and display it full screen, so
+ * that another person can scan the barcode from your screen.
+ */
+ public static final String ACTION = "com.google.zxing.client.android.ENCODE";
+
+ /**
+ * The data to encode. Use {@link android.content.Intent#putExtra(String, String)} or
+ * {@link android.content.Intent#putExtra(String, android.os.Bundle)},
+ * depending on the type and format specified. Non-QR Code formats should
+ * just use a String here. For QR Code, see Contents for details.
+ */
+ public static final String DATA = "ENCODE_DATA";
+
+ /**
+ * The type of data being supplied if the format is QR Code. Use
+ * {@link android.content.Intent#putExtra(String, String)} with one of {@link Contents.Type}.
+ */
+ public static final String TYPE = "ENCODE_TYPE";
+
+ /**
+ * The barcode format to be displayed. If this isn't specified or is blank,
+ * it defaults to QR Code. Use {@link android.content.Intent#putExtra(String, String)}, where
+ * format is one of {@link com.google.zxing.BarcodeFormat}.
+ */
+ public static final String FORMAT = "ENCODE_FORMAT";
+
+ /**
+ * Normally the contents of the barcode are displayed to the user in a TextView. Setting this
+ * boolean to false will hide that TextView, showing only the encode barcode.
+ */
+ public static final String SHOW_CONTENTS = "ENCODE_SHOW_CONTENTS";
+
+ private Encode() {
+ }
+ }
+
+ public static final class SearchBookContents {
+ /**
+ * Use Google Book Search to search the contents of the book provided.
+ */
+ public static final String ACTION = "com.google.zxing.client.android.SEARCH_BOOK_CONTENTS";
+
+ /**
+ * The book to search, identified by ISBN number.
+ */
+ public static final String ISBN = "ISBN";
+
+ /**
+ * An optional field which is the text to search for.
+ */
+ public static final String QUERY = "QUERY";
+
+ private SearchBookContents() {
+ }
+ }
+
+ public static final class WifiConnect {
+ /**
+ * Internal intent used to trigger connection to a wi-fi network.
+ */
+ public static final String ACTION = "com.google.zxing.client.android.WIFI_CONNECT";
+
+ /**
+ * The network to connect to, all the configuration provided here.
+ */
+ public static final String SSID = "SSID";
+
+ /**
+ * The network to connect to, all the configuration provided here.
+ */
+ public static final String TYPE = "TYPE";
+
+ /**
+ * The network to connect to, all the configuration provided here.
+ */
+ public static final String PASSWORD = "PASSWORD";
+
+ private WifiConnect() {
+ }
+ }
+
+ public static final class Share {
+ /**
+ * Give the user a choice of items to encode as a barcode, then render it as a QR Code and
+ * display onscreen for a friend to scan with their phone.
+ */
+ public static final String ACTION = "com.google.zxing.client.android.SHARE";
+
+ private Share() {
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/LocaleManager.java b/app/src/main/java/com/google/zxing/client/android/LocaleManager.java
new file mode 100644
index 0000000..e74f51d
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/LocaleManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2008 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 android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Handles any locale-specific logic for the client.
+ *
+ * @author Sean Owen
+ */
+public final class LocaleManager {
+
+ private static final String DEFAULT_TLD = "com";
+ private static final String DEFAULT_COUNTRY = "US";
+ private static final String DEFAULT_LANGUAGE = "en";
+
+ /**
+ * Locales (well, countries) where Google web search is available.
+ * These should be kept in sync with our translations.
+ */
+ private static final Map GOOGLE_COUNTRY_TLD;
+ static {
+ GOOGLE_COUNTRY_TLD = new HashMap<>();
+ GOOGLE_COUNTRY_TLD.put("AR", "com.ar"); // ARGENTINA
+ GOOGLE_COUNTRY_TLD.put("AU", "com.au"); // AUSTRALIA
+ GOOGLE_COUNTRY_TLD.put("BR", "com.br"); // BRAZIL
+ GOOGLE_COUNTRY_TLD.put("BG", "bg"); // BULGARIA
+ GOOGLE_COUNTRY_TLD.put(Locale.CANADA.getCountry(), "ca");
+ GOOGLE_COUNTRY_TLD.put(Locale.CHINA.getCountry(), "cn");
+ GOOGLE_COUNTRY_TLD.put("CZ", "cz"); // CZECH REPUBLIC
+ GOOGLE_COUNTRY_TLD.put("DK", "dk"); // DENMARK
+ GOOGLE_COUNTRY_TLD.put("FI", "fi"); // FINLAND
+ GOOGLE_COUNTRY_TLD.put(Locale.FRANCE.getCountry(), "fr");
+ GOOGLE_COUNTRY_TLD.put(Locale.GERMANY.getCountry(), "de");
+ GOOGLE_COUNTRY_TLD.put("GR", "gr"); // GREECE
+ GOOGLE_COUNTRY_TLD.put("HU", "hu"); // HUNGARY
+ GOOGLE_COUNTRY_TLD.put("ID", "co.id"); // INDONESIA
+ GOOGLE_COUNTRY_TLD.put("IL", "co.il"); // ISRAEL
+ GOOGLE_COUNTRY_TLD.put(Locale.ITALY.getCountry(), "it");
+ GOOGLE_COUNTRY_TLD.put(Locale.JAPAN.getCountry(), "co.jp");
+ GOOGLE_COUNTRY_TLD.put(Locale.KOREA.getCountry(), "co.kr");
+ GOOGLE_COUNTRY_TLD.put("NL", "nl"); // NETHERLANDS
+ GOOGLE_COUNTRY_TLD.put("PL", "pl"); // POLAND
+ GOOGLE_COUNTRY_TLD.put("PT", "pt"); // PORTUGAL
+ GOOGLE_COUNTRY_TLD.put("RO", "ro"); // ROMANIA
+ GOOGLE_COUNTRY_TLD.put("RU", "ru"); // RUSSIA
+ GOOGLE_COUNTRY_TLD.put("SK", "sk"); // SLOVAK REPUBLIC
+ GOOGLE_COUNTRY_TLD.put("SI", "si"); // SLOVENIA
+ GOOGLE_COUNTRY_TLD.put("ES", "es"); // SPAIN
+ GOOGLE_COUNTRY_TLD.put("SE", "se"); // SWEDEN
+ GOOGLE_COUNTRY_TLD.put("CH", "ch"); // SWITZERLAND
+ GOOGLE_COUNTRY_TLD.put(Locale.TAIWAN.getCountry(), "tw");
+ GOOGLE_COUNTRY_TLD.put("TR", "com.tr"); // TURKEY
+ GOOGLE_COUNTRY_TLD.put("UA", "com.ua"); // UKRAINE
+ GOOGLE_COUNTRY_TLD.put(Locale.UK.getCountry(), "co.uk");
+ GOOGLE_COUNTRY_TLD.put(Locale.US.getCountry(), "com");
+ }
+
+ /**
+ * Google Product Search for mobile is available in fewer countries than web search. See here:
+ * http://support.google.com/merchants/bin/answer.py?hl=en-GB&answer=160619
+ */
+ private static final Map GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD;
+ static {
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD = new HashMap<>();
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("AU", "com.au"); // AUSTRALIA
+ //GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.CHINA.getCountry(), "cn");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.FRANCE.getCountry(), "fr");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.GERMANY.getCountry(), "de");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.ITALY.getCountry(), "it");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.JAPAN.getCountry(), "co.jp");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("NL", "nl"); // NETHERLANDS
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("ES", "es"); // SPAIN
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put("CH", "ch"); // SWITZERLAND
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.UK.getCountry(), "co.uk");
+ GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD.put(Locale.US.getCountry(), "com");
+ }
+
+ /**
+ * Book search is offered everywhere that web search is available.
+ */
+ private static final Map GOOGLE_BOOK_SEARCH_COUNTRY_TLD = GOOGLE_COUNTRY_TLD;
+
+ private static final Collection TRANSLATED_HELP_ASSET_LANGUAGES =
+ Arrays.asList("de", "en", "es", "fr", "it", "ja", "ko", "nl", "pt", "ru", "uk", "zh-rCN", "zh-rTW", "zh-rHK");
+
+ private LocaleManager() {}
+
+ /**
+ * @param context application's {@link Context}
+ * @return country-specific TLD suffix appropriate for the current default locale
+ * (e.g. "co.uk" for the United Kingdom)
+ */
+ public static String getCountryTLD(Context context) {
+ return doGetTLD(GOOGLE_COUNTRY_TLD, context);
+ }
+
+ /**
+ * The same as above, but specifically for Google Product Search.
+ *
+ * @param context application's {@link Context}
+ * @return The top-level domain to use.
+ */
+ public static String getProductSearchCountryTLD(Context context) {
+ return doGetTLD(GOOGLE_PRODUCT_SEARCH_COUNTRY_TLD, context);
+ }
+
+ /**
+ * The same as above, but specifically for Google Book Search.
+ *
+ * @param context application's {@link Context}
+ * @return The top-level domain to use.
+ */
+ public static String getBookSearchCountryTLD(Context context) {
+ return doGetTLD(GOOGLE_BOOK_SEARCH_COUNTRY_TLD, context);
+ }
+
+ /**
+ * Does a given URL point to Google Book Search, regardless of domain.
+ *
+ * @param url The address to check.
+ * @return True if this is a Book Search URL.
+ */
+ public static boolean isBookSearchUrl(String url) {
+ return url.startsWith("http://google.com/books") || url.startsWith("http://books.google.");
+ }
+
+ private static String getSystemCountry() {
+ Locale locale = Locale.getDefault();
+ return locale == null ? DEFAULT_COUNTRY : locale.getCountry();
+ }
+
+ private static String getSystemLanguage() {
+ Locale locale = Locale.getDefault();
+ if (locale == null) {
+ return DEFAULT_LANGUAGE;
+ }
+ String language = locale.getLanguage();
+ // Special case Chinese
+ if (Locale.SIMPLIFIED_CHINESE.getLanguage().equals(language)) {
+ return language + "-r" + getSystemCountry();
+ }
+ return language;
+ }
+
+ public static String getTranslatedAssetLanguage() {
+ String language = getSystemLanguage();
+ return TRANSLATED_HELP_ASSET_LANGUAGES.contains(language) ? language : DEFAULT_LANGUAGE;
+ }
+
+ private static String doGetTLD(Map map, Context context) {
+ String tld = map.get(getCountry(context));
+ return tld == null ? DEFAULT_TLD : tld;
+ }
+
+ public static String getCountry(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String countryOverride = prefs.getString(PreferencesActivity.KEY_SEARCH_COUNTRY, "-");
+ if (countryOverride != null && !countryOverride.isEmpty() && !"-".equals(countryOverride)) {
+ return countryOverride;
+ }
+ return getSystemCountry();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java b/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java
new file mode 100644
index 0000000..8d0dc64
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/PreferencesActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2008 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 android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * The main settings activity.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ * @author Sean Owen
+ */
+public final class PreferencesActivity extends Activity {
+
+ public static final String KEY_DECODE_1D_PRODUCT = "preferences_decode_1D_product";
+ public static final String KEY_DECODE_1D_INDUSTRIAL = "preferences_decode_1D_industrial";
+ public static final String KEY_DECODE_QR = "preferences_decode_QR";
+ public static final String KEY_DECODE_DATA_MATRIX = "preferences_decode_Data_Matrix";
+ public static final String KEY_DECODE_AZTEC = "preferences_decode_Aztec";
+ public static final String KEY_DECODE_PDF417 = "preferences_decode_PDF417";
+
+ public static final String KEY_CUSTOM_PRODUCT_SEARCH = "preferences_custom_product_search";
+
+ public static final String KEY_PLAY_BEEP = "preferences_play_beep";
+ public static final String KEY_VIBRATE = "preferences_vibrate";
+ public static final String KEY_COPY_TO_CLIPBOARD = "preferences_copy_to_clipboard";
+ public static final String KEY_FRONT_LIGHT_MODE = "preferences_front_light_mode";
+ public static final String KEY_BULK_MODE = "preferences_bulk_mode";
+ public static final String KEY_REMEMBER_DUPLICATES = "preferences_remember_duplicates";
+ public static final String KEY_ENABLE_HISTORY = "preferences_history";
+ public static final String KEY_SUPPLEMENTAL = "preferences_supplemental";
+ public static final String KEY_AUTO_FOCUS = "preferences_auto_focus";
+ public static final String KEY_INVERT_SCAN = "preferences_invert_scan";
+ public static final String KEY_SEARCH_COUNTRY = "preferences_search_country";
+ public static final String KEY_DISABLE_AUTO_ORIENTATION = "preferences_orientation";
+
+ public static final String KEY_DISABLE_CONTINUOUS_FOCUS = "preferences_disable_continuous_focus";
+ public static final String KEY_DISABLE_EXPOSURE = "preferences_disable_exposure";
+ public static final String KEY_DISABLE_METERING = "preferences_disable_metering";
+ public static final String KEY_DISABLE_BARCODE_SCENE_MODE = "preferences_disable_barcode_scene_mode";
+ public static final String KEY_AUTO_OPEN_WEB = "preferences_auto_open_web";
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ getFragmentManager().beginTransaction().replace(android.R.id.content, new PreferencesFragment()).commit();
+ }
+
+ // Apparently this will be necessary when targeting API 19+:
+ /*
+ @Override
+ protected boolean isValidFragment(String fragmentName) {
+ return true;
+ }
+ */
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java b/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java
new file mode 100644
index 0000000..6d90ef7
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/PreferencesFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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 android.app.AlertDialog;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+
+import net.foucry.pilldroid.R;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public final class PreferencesFragment
+ extends PreferenceFragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private CheckBoxPreference[] checkBoxPrefs;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.preferences);
+
+ PreferenceScreen preferences = getPreferenceScreen();
+ preferences.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ checkBoxPrefs = findDecodePrefs(preferences,
+ PreferencesActivity.KEY_DECODE_1D_PRODUCT,
+ PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL,
+ PreferencesActivity.KEY_DECODE_QR,
+ PreferencesActivity.KEY_DECODE_DATA_MATRIX,
+ PreferencesActivity.KEY_DECODE_AZTEC,
+ PreferencesActivity.KEY_DECODE_PDF417);
+ disableLastCheckedPref();
+
+ EditTextPreference customProductSearch = (EditTextPreference)
+ preferences.findPreference(PreferencesActivity.KEY_CUSTOM_PRODUCT_SEARCH);
+ customProductSearch.setOnPreferenceChangeListener(new CustomSearchURLValidator());
+ }
+
+ private static CheckBoxPreference[] findDecodePrefs(PreferenceScreen preferences, String... keys) {
+ CheckBoxPreference[] prefs = new CheckBoxPreference[keys.length];
+ for (int i = 0; i < keys.length; i++) {
+ prefs[i] = (CheckBoxPreference) preferences.findPreference(keys[i]);
+ }
+ return prefs;
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ disableLastCheckedPref();
+ }
+
+ private void disableLastCheckedPref() {
+ Collection checked = new ArrayList<>(checkBoxPrefs.length);
+ for (CheckBoxPreference pref : checkBoxPrefs) {
+ if (pref.isChecked()) {
+ checked.add(pref);
+ }
+ }
+ boolean disable = checked.size() <= 1;
+ for (CheckBoxPreference pref : checkBoxPrefs) {
+ pref.setEnabled(!(disable && checked.contains(pref)));
+ }
+ }
+
+ private class CustomSearchURLValidator implements Preference.OnPreferenceChangeListener {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (!isValid(newValue)) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(PreferencesFragment.this.getActivity());
+ builder.setTitle(R.string.msg_error);
+ builder.setMessage(R.string.msg_invalid_value);
+ builder.setCancelable(true);
+ builder.show();
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValid(Object newValue) {
+ // Allow empty/null value
+ if (newValue == null) {
+ return true;
+ }
+ String valueString = newValue.toString();
+ if (valueString.isEmpty()) {
+ return true;
+ }
+ // Before validating, remove custom placeholders, which will not
+ // be considered valid parts of the URL in some locations:
+ // Blank %t and %s:
+ valueString = valueString.replaceAll("%[st]", "");
+ // Blank %f but not if followed by digit or a-f as it may be a hex sequence
+ valueString = valueString.replaceAll("%f(?![0-9a-f])", "");
+ // Require a scheme otherwise:
+ try {
+ URI uri = new URI(valueString);
+ return uri.getScheme() != null;
+ } catch (URISyntaxException use) {
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java b/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java
new file mode 100644
index 0000000..15170b0
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/ScanFromWebPageManager.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 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 android.net.Uri;
+import com.google.zxing.Result;
+import com.google.zxing.client.android.result.ResultHandler;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+/**
+ * Manages functionality related to responding to requests to scan from an HTTP link in a web page.
+ * See ScanningFromWebPages .
+ *
+ * @author Sean Owen
+ */
+final class ScanFromWebPageManager {
+
+ private static final CharSequence CODE_PLACEHOLDER = "{CODE}";
+ private static final CharSequence RAW_CODE_PLACEHOLDER = "{RAWCODE}";
+ private static final CharSequence META_PLACEHOLDER = "{META}";
+ private static final CharSequence FORMAT_PLACEHOLDER = "{FORMAT}";
+ private static final CharSequence TYPE_PLACEHOLDER = "{TYPE}";
+
+ private static final String RETURN_URL_PARAM = "ret";
+ private static final String RAW_PARAM = "raw";
+
+ private final String returnUrlTemplate;
+ private final boolean returnRaw;
+
+ ScanFromWebPageManager(Uri inputUri) {
+ returnUrlTemplate = inputUri.getQueryParameter(RETURN_URL_PARAM);
+ returnRaw = inputUri.getQueryParameter(RAW_PARAM) != null;
+ }
+
+ boolean isScanFromWebPage() {
+ return returnUrlTemplate != null;
+ }
+
+ String buildReplyURL(Result rawResult, ResultHandler resultHandler) {
+ String result = returnUrlTemplate;
+ result = replace(CODE_PLACEHOLDER,
+ returnRaw ? rawResult.getText() : resultHandler.getDisplayContents(), result);
+ result = replace(RAW_CODE_PLACEHOLDER, rawResult.getText(), result);
+ result = replace(FORMAT_PLACEHOLDER, rawResult.getBarcodeFormat().toString(), result);
+ result = replace(TYPE_PLACEHOLDER, resultHandler.getType().toString(), result);
+ result = replace(META_PLACEHOLDER, String.valueOf(rawResult.getResultMetadata()), result);
+ return result;
+ }
+
+ private static String replace(CharSequence placeholder, CharSequence with, String pattern) {
+ CharSequence escapedWith = with == null ? "" : with;
+ try {
+ escapedWith = URLEncoder.encode(escapedWith.toString(), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // can't happen; UTF-8 is always supported. Continue, I guess, without encoding
+ }
+ return pattern.replace(placeholder, escapedWith);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java b/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java
new file mode 100644
index 0000000..48009a9
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/ViewfinderResultPointCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2009 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 com.google.zxing.ResultPoint;
+import com.google.zxing.ResultPointCallback;
+
+final class ViewfinderResultPointCallback implements ResultPointCallback {
+
+ private final ViewfinderView viewfinderView;
+
+ ViewfinderResultPointCallback(ViewfinderView viewfinderView) {
+ this.viewfinderView = viewfinderView;
+ }
+
+ @Override
+ public void foundPossibleResultPoint(ResultPoint point) {
+ viewfinderView.addPossibleResultPoint(point);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java b/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java
new file mode 100644
index 0000000..4aa499d
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/ViewfinderView.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2008 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 android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.google.zxing.ResultPoint;
+import com.google.zxing.client.android.camera.CameraManager;
+
+import net.foucry.pilldroid.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This view is overlaid on top of the camera preview. It adds the viewfinder rectangle and partial
+ * transparency outside it, as well as the laser scanner animation and result points.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ViewfinderView extends View {
+
+ private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64};
+ private static final long ANIMATION_DELAY = 80L;
+ private static final int CURRENT_POINT_OPACITY = 0xA0;
+ private static final int MAX_RESULT_POINTS = 20;
+ private static final int POINT_SIZE = 6;
+
+ private CameraManager cameraManager;
+ private final Paint paint;
+ private Bitmap resultBitmap;
+ private final int maskColor;
+ private final int resultColor;
+ private final int laserColor;
+ private final int resultPointColor;
+ private int scannerAlpha;
+ private List possibleResultPoints;
+ private List lastPossibleResultPoints;
+
+ // This constructor is used when the class is built from an XML resource.
+ public ViewfinderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Initialize these once for performance rather than calling them every time in onDraw().
+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ Resources resources = getResources();
+ maskColor = resources.getColor(R.color.viewfinder_mask);
+ resultColor = resources.getColor(R.color.result_view);
+ laserColor = resources.getColor(R.color.viewfinder_laser);
+ resultPointColor = resources.getColor(R.color.possible_result_points);
+ scannerAlpha = 0;
+ possibleResultPoints = new ArrayList<>(5);
+ lastPossibleResultPoints = null;
+ }
+
+ public void setCameraManager(CameraManager cameraManager) {
+ this.cameraManager = cameraManager;
+ }
+
+ @SuppressLint("DrawAllocation")
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (cameraManager == null) {
+ return; // not ready yet, early draw before done configuring
+ }
+ Rect frame = cameraManager.getFramingRect();
+ Rect previewFrame = cameraManager.getFramingRectInPreview();
+ if (frame == null || previewFrame == null) {
+ return;
+ }
+ int width = canvas.getWidth();
+ int height = canvas.getHeight();
+
+ // Draw the exterior (i.e. outside the framing rect) darkened
+ paint.setColor(resultBitmap != null ? resultColor : maskColor);
+ canvas.drawRect(0, 0, width, frame.top, paint);
+ canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
+ canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
+ canvas.drawRect(0, frame.bottom + 1, width, height, paint);
+
+ if (resultBitmap != null) {
+ // Draw the opaque result bitmap over the scanning rectangle
+ paint.setAlpha(CURRENT_POINT_OPACITY);
+ canvas.drawBitmap(resultBitmap, null, frame, paint);
+ } else {
+
+ // Draw a red "laser scanner" line through the middle to show decoding is active
+ paint.setColor(laserColor);
+ paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
+ scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
+ int middle = frame.height() / 2 + frame.top;
+ canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1, middle + 2, paint);
+
+ float scaleX = frame.width() / (float) previewFrame.width();
+ float scaleY = frame.height() / (float) previewFrame.height();
+
+ List currentPossible = possibleResultPoints;
+ List currentLast = lastPossibleResultPoints;
+ int frameLeft = frame.left;
+ int frameTop = frame.top;
+ if (currentPossible.isEmpty()) {
+ lastPossibleResultPoints = null;
+ } else {
+ possibleResultPoints = new ArrayList<>(5);
+ lastPossibleResultPoints = currentPossible;
+ paint.setAlpha(CURRENT_POINT_OPACITY);
+ paint.setColor(resultPointColor);
+ synchronized (currentPossible) {
+ for (ResultPoint point : currentPossible) {
+ canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
+ frameTop + (int) (point.getY() * scaleY),
+ POINT_SIZE, paint);
+ }
+ }
+ }
+ if (currentLast != null) {
+ paint.setAlpha(CURRENT_POINT_OPACITY / 2);
+ paint.setColor(resultPointColor);
+ synchronized (currentLast) {
+ float radius = POINT_SIZE / 2.0f;
+ for (ResultPoint point : currentLast) {
+ canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
+ frameTop + (int) (point.getY() * scaleY),
+ radius, paint);
+ }
+ }
+ }
+
+ // Request another update at the animation interval, but only repaint the laser line,
+ // not the entire viewfinder mask.
+ postInvalidateDelayed(ANIMATION_DELAY,
+ frame.left - POINT_SIZE,
+ frame.top - POINT_SIZE,
+ frame.right + POINT_SIZE,
+ frame.bottom + POINT_SIZE);
+ }
+ }
+
+ public void drawViewfinder() {
+ Bitmap resultBitmap = this.resultBitmap;
+ this.resultBitmap = null;
+ if (resultBitmap != null) {
+ resultBitmap.recycle();
+ }
+ invalidate();
+ }
+
+ /**
+ * Draw a bitmap with the result points highlighted instead of the live scanning display.
+ *
+ * @param barcode An image of the decoded barcode.
+ */
+ public void drawResultBitmap(Bitmap barcode) {
+ resultBitmap = barcode;
+ invalidate();
+ }
+
+ public void addPossibleResultPoint(ResultPoint point) {
+ List points = possibleResultPoints;
+ synchronized (points) {
+ points.add(point);
+ int size = points.size();
+ if (size > MAX_RESULT_POINTS) {
+ // trim it
+ points.subList(0, size - MAX_RESULT_POINTS / 2).clear();
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java b/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java
new file mode 100644
index 0000000..c00bd6e
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/book/BrowseBookListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 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.book;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.View;
+import android.widget.AdapterView;
+import com.google.zxing.client.android.LocaleManager;
+
+import java.util.List;
+
+final class BrowseBookListener implements AdapterView.OnItemClickListener {
+
+ private final SearchBookContentsActivity activity;
+ private final List items;
+
+ BrowseBookListener(SearchBookContentsActivity activity, List items) {
+ this.activity = activity;
+ this.items = items;
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ if (position < 1) {
+ // Clicked header, ignore it
+ return;
+ }
+ int itemOffset = position - 1;
+ if (itemOffset >= items.size()) {
+ return;
+ }
+ String pageId = items.get(itemOffset).getPageId();
+ String query = SearchBookContentsResult.getQuery();
+ if (LocaleManager.isBookSearchUrl(activity.getISBN()) && !pageId.isEmpty()) {
+ String uri = activity.getISBN();
+ int equals = uri.indexOf('=');
+ String volumeId = uri.substring(equals + 1);
+ String readBookURI = "http://books.google." +
+ LocaleManager.getBookSearchCountryTLD(activity) +
+ "/books?id=" + volumeId + "&pg=" + pageId + "&vq=" + query;
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(readBookURI));
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ activity.startActivity(intent);
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java
new file mode 100644
index 0000000..9c14cfb
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsActivity.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2008 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.book;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.webkit.CookieManager;
+import android.webkit.CookieSyncManager;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.zxing.client.android.HttpHelper;
+import com.google.zxing.client.android.Intents;
+import com.google.zxing.client.android.LocaleManager;
+
+import net.foucry.pilldroid.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+/**
+ * Uses Google Book Search to find a word or phrase in the requested book.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class SearchBookContentsActivity extends Activity {
+
+ private static final String TAG = SearchBookContentsActivity.class.getSimpleName();
+
+ private static final Pattern TAG_PATTERN = Pattern.compile("\\<.*?\\>");
+ private static final Pattern LT_ENTITY_PATTERN = Pattern.compile("<");
+ private static final Pattern GT_ENTITY_PATTERN = Pattern.compile(">");
+ private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("'");
+ private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile(""");
+
+ private String isbn;
+ private EditText queryTextView;
+ private View queryButton;
+ private ListView resultListView;
+ private TextView headerView;
+ private AsyncTask networkTask;
+
+ private final View.OnClickListener buttonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ launchSearch();
+ }
+ };
+
+ private final View.OnKeyListener keyListener = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
+ launchSearch();
+ return true;
+ }
+ return false;
+ }
+ };
+
+ String getISBN() {
+ return isbn;
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Make sure that expired cookies are removed on launch.
+ CookieSyncManager.createInstance(this);
+ CookieManager.getInstance().removeExpiredCookie();
+
+ Intent intent = getIntent();
+ if (intent == null || !Intents.SearchBookContents.ACTION.equals(intent.getAction())) {
+ finish();
+ return;
+ }
+
+ isbn = intent.getStringExtra(Intents.SearchBookContents.ISBN);
+ if (LocaleManager.isBookSearchUrl(isbn)) {
+ setTitle(getString(R.string.sbc_name));
+ } else {
+ setTitle(getString(R.string.sbc_name) + ": ISBN " + isbn);
+ }
+
+ setContentView(R.layout.search_book_contents);
+ queryTextView = (EditText) findViewById(R.id.query_text_view);
+
+ String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY);
+ if (initialQuery != null && !initialQuery.isEmpty()) {
+ // Populate the search box but don't trigger the search
+ queryTextView.setText(initialQuery);
+ }
+ queryTextView.setOnKeyListener(keyListener);
+
+ queryButton = findViewById(R.id.query_button);
+ queryButton.setOnClickListener(buttonListener);
+
+ resultListView = (ListView) findViewById(R.id.result_list_view);
+ LayoutInflater factory = LayoutInflater.from(this);
+ headerView = (TextView) factory.inflate(R.layout.search_book_contents_header,
+ resultListView, false);
+ resultListView.addHeaderView(headerView);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ queryTextView.selectAll();
+ }
+
+ @Override
+ protected void onPause() {
+ AsyncTask,?,?> oldTask = networkTask;
+ if (oldTask != null) {
+ oldTask.cancel(true);
+ networkTask = null;
+ }
+ super.onPause();
+ }
+
+ private void launchSearch() {
+ String query = queryTextView.getText().toString();
+ if (query != null && !query.isEmpty()) {
+ AsyncTask,?,?> oldTask = networkTask;
+ if (oldTask != null) {
+ oldTask.cancel(true);
+ }
+ networkTask = new NetworkTask();
+ networkTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, query, isbn);
+ headerView.setText(R.string.msg_sbc_searching_book);
+ resultListView.setAdapter(null);
+ queryTextView.setEnabled(false);
+ queryButton.setEnabled(false);
+ }
+ }
+
+ private final class NetworkTask extends AsyncTask {
+
+ @Override
+ protected JSONObject doInBackground(String... args) {
+ try {
+ // These return a JSON result which describes if and where the query was found. This API may
+ // break or disappear at any time in the future. Since this is an API call rather than a
+ // website, we don't use LocaleManager to change the TLD.
+ String theQuery = args[0];
+ String theIsbn = args[1];
+ String uri;
+ if (LocaleManager.isBookSearchUrl(theIsbn)) {
+ int equals = theIsbn.indexOf('=');
+ String volumeId = theIsbn.substring(equals + 1);
+ uri = "http://www.google.com/books?id=" + volumeId + "&jscmd=SearchWithinVolume2&q=" + theQuery;
+ } else {
+ uri = "http://www.google.com/books?vid=isbn" + theIsbn + "&jscmd=SearchWithinVolume2&q=" + theQuery;
+ }
+ CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.JSON);
+ return new JSONObject(content.toString());
+ } catch (IOException ioe) {
+ Log.w(TAG, "Error accessing book search", ioe);
+ return null;
+ } catch (JSONException je) {
+ Log.w(TAG, "Error accessing book search", je);
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject result) {
+ if (result == null) {
+ headerView.setText(R.string.msg_sbc_failed);
+ } else {
+ handleSearchResults(result);
+ }
+ queryTextView.setEnabled(true);
+ queryTextView.selectAll();
+ queryButton.setEnabled(true);
+ }
+
+ // Currently there is no way to distinguish between a query which had no results and a book
+ // which is not searchable - both return zero results.
+ private void handleSearchResults(JSONObject json) {
+ try {
+ int count = json.getInt("number_of_results");
+ headerView.setText(getString(R.string.msg_sbc_results) + " : " + count);
+ if (count > 0) {
+ JSONArray results = json.getJSONArray("search_results");
+ SearchBookContentsResult.setQuery(queryTextView.getText().toString());
+ List items = new ArrayList<>(count);
+ for (int x = 0; x < count; x++) {
+ items.add(parseResult(results.getJSONObject(x)));
+ }
+ resultListView.setOnItemClickListener(new BrowseBookListener(SearchBookContentsActivity.this, items));
+ resultListView.setAdapter(new SearchBookContentsAdapter(SearchBookContentsActivity.this, items));
+ } else {
+ String searchable = json.optString("searchable");
+ if ("false".equals(searchable)) {
+ headerView.setText(R.string.msg_sbc_book_not_searchable);
+ }
+ resultListView.setAdapter(null);
+ }
+ } catch (JSONException e) {
+ Log.w(TAG, "Bad JSON from book search", e);
+ resultListView.setAdapter(null);
+ headerView.setText(R.string.msg_sbc_failed);
+ }
+ }
+
+ // Available fields: page_id, page_number, snippet_text
+ private SearchBookContentsResult parseResult(JSONObject json) {
+
+ String pageId;
+ String pageNumber;
+ String snippet;
+ try {
+ pageId = json.getString("page_id");
+ pageNumber = json.optString("page_number");
+ snippet = json.optString("snippet_text");
+ } catch (JSONException e) {
+ Log.w(TAG, e);
+ // Never seen in the wild, just being complete.
+ return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", "", false);
+ }
+
+ if (pageNumber == null || pageNumber.isEmpty()) {
+ // This can happen for text on the jacket, and possibly other reasons.
+ pageNumber = "";
+ } else {
+ pageNumber = getString(R.string.msg_sbc_page) + ' ' + pageNumber;
+ }
+
+ boolean valid = snippet != null && !snippet.isEmpty();
+ if (valid) {
+ // Remove all HTML tags and encoded characters.
+ snippet = TAG_PATTERN.matcher(snippet).replaceAll("");
+ snippet = LT_ENTITY_PATTERN.matcher(snippet).replaceAll("<");
+ snippet = GT_ENTITY_PATTERN.matcher(snippet).replaceAll(">");
+ snippet = QUOTE_ENTITY_PATTERN.matcher(snippet).replaceAll("'");
+ snippet = QUOT_ENTITY_PATTERN.matcher(snippet).replaceAll("\"");
+ } else {
+ snippet = '(' + getString(R.string.msg_sbc_snippet_unavailable) + ')';
+ }
+
+ return new SearchBookContentsResult(pageId, pageNumber, snippet, valid);
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java
new file mode 100644
index 0000000..84f5889
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsAdapter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 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.book;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import net.foucry.pilldroid.R;
+
+import java.util.List;
+/**
+ * Manufactures list items which represent SBC results.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class SearchBookContentsAdapter extends ArrayAdapter {
+
+ SearchBookContentsAdapter(Context context, List items) {
+ super(context, R.layout.search_book_contents_list_item, 0, items);
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup viewGroup) {
+ SearchBookContentsListItem listItem;
+
+ if (view == null) {
+ LayoutInflater factory = LayoutInflater.from(getContext());
+ listItem = (SearchBookContentsListItem) factory.inflate(
+ R.layout.search_book_contents_list_item, viewGroup, false);
+ } else {
+ if (view instanceof SearchBookContentsListItem) {
+ listItem = (SearchBookContentsListItem) view;
+ } else {
+ return view;
+ }
+ }
+
+ SearchBookContentsResult result = getItem(position);
+ listItem.set(result);
+ return listItem;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java
new file mode 100644
index 0000000..5ef5d58
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsListItem.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2008 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.book;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.foucry.pilldroid.R;
+
+import java.util.Locale;
+
+/**
+ * A list item which displays the page number and snippet of this search result.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class SearchBookContentsListItem extends LinearLayout {
+ private TextView pageNumberView;
+ private TextView snippetView;
+
+ SearchBookContentsListItem(Context context) {
+ super(context);
+ }
+
+ public SearchBookContentsListItem(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ pageNumberView = (TextView) findViewById(R.id.page_number_view);
+ snippetView = (TextView) findViewById(R.id.snippet_view);
+ }
+
+ public void set(SearchBookContentsResult result) {
+ pageNumberView.setText(result.getPageNumber());
+ String snippet = result.getSnippet();
+ if (snippet.isEmpty()) {
+ snippetView.setText("");
+ } else {
+ if (result.getValidSnippet()) {
+ String lowerQuery = SearchBookContentsResult.getQuery().toLowerCase(Locale.getDefault());
+ String lowerSnippet = snippet.toLowerCase(Locale.getDefault());
+ Spannable styledSnippet = new SpannableString(snippet);
+ StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
+ int queryLength = lowerQuery.length();
+ int offset = 0;
+ while (true) {
+ int pos = lowerSnippet.indexOf(lowerQuery, offset);
+ if (pos < 0) {
+ break;
+ }
+ styledSnippet.setSpan(boldSpan, pos, pos + queryLength, 0);
+ offset = pos + queryLength;
+ }
+ snippetView.setText(styledSnippet);
+ } else {
+ // This may be an error message, so don't try to bold the query terms within it
+ snippetView.setText(snippet);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java
new file mode 100644
index 0000000..ffaafb3
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/book/SearchBookContentsResult.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2008 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.book;
+
+/**
+ * The underlying data for a SBC result.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class SearchBookContentsResult {
+
+ private static String query = null;
+
+ private final String pageId;
+ private final String pageNumber;
+ private final String snippet;
+ private final boolean validSnippet;
+
+ SearchBookContentsResult(String pageId,
+ String pageNumber,
+ String snippet,
+ boolean validSnippet) {
+ this.pageId = pageId;
+ this.pageNumber = pageNumber;
+ this.snippet = snippet;
+ this.validSnippet = validSnippet;
+ }
+
+ public static void setQuery(String query) {
+ SearchBookContentsResult.query = query;
+ }
+
+ public String getPageId() {
+ return pageId;
+ }
+
+ public String getPageNumber() {
+ return pageNumber;
+ }
+
+ public String getSnippet() {
+ return snippet;
+ }
+
+ public boolean getValidSnippet() {
+ return validSnippet;
+ }
+
+ public static String getQuery() {
+ return query;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java b/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java
new file mode 100644
index 0000000..05aee0a
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.hardware.Camera;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.RejectedExecutionException;
+
+import com.google.zxing.client.android.PreferencesActivity;
+
+final class AutoFocusManager implements Camera.AutoFocusCallback {
+
+ private static final String TAG = AutoFocusManager.class.getSimpleName();
+
+ private static final long AUTO_FOCUS_INTERVAL_MS = 2000L;
+ private static final Collection FOCUS_MODES_CALLING_AF;
+ static {
+ FOCUS_MODES_CALLING_AF = new ArrayList<>(2);
+ FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_AUTO);
+ FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO);
+ }
+
+ private boolean stopped;
+ private boolean focusing;
+ private final boolean useAutoFocus;
+ private final Camera camera;
+ private AsyncTask,?,?> outstandingTask;
+
+ AutoFocusManager(Context context, Camera camera) {
+ this.camera = camera;
+ SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String currentFocusMode = camera.getParameters().getFocusMode();
+ useAutoFocus =
+ sharedPrefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true) &&
+ FOCUS_MODES_CALLING_AF.contains(currentFocusMode);
+ Log.i(TAG, "Current focus mode '" + currentFocusMode + "'; use auto focus? " + useAutoFocus);
+ start();
+ }
+
+ @Override
+ public synchronized void onAutoFocus(boolean success, Camera theCamera) {
+ focusing = false;
+ autoFocusAgainLater();
+ }
+
+ private synchronized void autoFocusAgainLater() {
+ if (!stopped && outstandingTask == null) {
+ AutoFocusTask newTask = new AutoFocusTask();
+ try {
+ newTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ outstandingTask = newTask;
+ } catch (RejectedExecutionException ree) {
+ Log.w(TAG, "Could not request auto focus", ree);
+ }
+ }
+ }
+
+ synchronized void start() {
+ if (useAutoFocus) {
+ outstandingTask = null;
+ if (!stopped && !focusing) {
+ try {
+ camera.autoFocus(this);
+ focusing = true;
+ } catch (RuntimeException re) {
+ // Have heard RuntimeException reported in Android 4.0.x+; continue?
+ Log.w(TAG, "Unexpected exception while focusing", re);
+ // Try again later to keep cycle going
+ autoFocusAgainLater();
+ }
+ }
+ }
+ }
+
+ private synchronized void cancelOutstandingTask() {
+ if (outstandingTask != null) {
+ if (outstandingTask.getStatus() != AsyncTask.Status.FINISHED) {
+ outstandingTask.cancel(true);
+ }
+ outstandingTask = null;
+ }
+ }
+
+ synchronized void stop() {
+ stopped = true;
+ if (useAutoFocus) {
+ cancelOutstandingTask();
+ // Doesn't hurt to call this even if not focusing
+ try {
+ camera.cancelAutoFocus();
+ } catch (RuntimeException re) {
+ // Have heard RuntimeException reported in Android 4.0.x+; continue?
+ Log.w(TAG, "Unexpected exception while cancelling focusing", re);
+ }
+ }
+ }
+
+ private final class AutoFocusTask extends AsyncTask {
+ @Override
+ protected Object doInBackground(Object... voids) {
+ try {
+ Thread.sleep(AUTO_FOCUS_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ // continue
+ }
+ start();
+ return null;
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java b/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java
new file mode 100644
index 0000000..a18e37e
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2010 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.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Point;
+import android.hardware.Camera;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.google.zxing.client.android.PreferencesActivity;
+import com.google.zxing.client.android.camera.open.CameraFacing;
+import com.google.zxing.client.android.camera.open.OpenCamera;
+
+/**
+ * A class which deals with reading, parsing, and setting the camera parameters which are used to
+ * configure the camera hardware.
+ */
+final class CameraConfigurationManager {
+
+ private static final String TAG = "CameraConfiguration";
+
+ private final Context context;
+ private int cwNeededRotation;
+ private int cwRotationFromDisplayToCamera;
+ private Point screenResolution;
+ private Point cameraResolution;
+ private Point bestPreviewSize;
+ private Point previewSizeOnScreen;
+
+ CameraConfigurationManager(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Reads, one time, values from the camera that are needed by the app.
+ */
+ void initFromCameraParameters(OpenCamera camera) {
+ Camera.Parameters parameters = camera.getCamera().getParameters();
+ WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = manager.getDefaultDisplay();
+
+ int displayRotation = display.getRotation();
+ int cwRotationFromNaturalToDisplay;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ cwRotationFromNaturalToDisplay = 0;
+ break;
+ case Surface.ROTATION_90:
+ cwRotationFromNaturalToDisplay = 90;
+ break;
+ case Surface.ROTATION_180:
+ cwRotationFromNaturalToDisplay = 180;
+ break;
+ case Surface.ROTATION_270:
+ cwRotationFromNaturalToDisplay = 270;
+ break;
+ default:
+ // Have seen this return incorrect values like -90
+ if (displayRotation % 90 == 0) {
+ cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
+ } else {
+ throw new IllegalArgumentException("Bad rotation: " + displayRotation);
+ }
+ }
+ Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);
+
+ int cwRotationFromNaturalToCamera = camera.getOrientation();
+ Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);
+
+ // Still not 100% sure about this. But acts like we need to flip this:
+ if (camera.getFacing() == CameraFacing.FRONT) {
+ cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
+ Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
+ }
+
+ /*
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String overrideRotationString;
+ if (camera.getFacing() == CameraFacing.FRONT) {
+ overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null);
+ } else {
+ overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION, null);
+ }
+ if (overrideRotationString != null && !"-".equals(overrideRotationString)) {
+ Log.i(TAG, "Overriding camera manually to " + overrideRotationString);
+ cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString);
+ }
+ */
+
+ cwRotationFromDisplayToCamera =
+ (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
+ Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
+ if (camera.getFacing() == CameraFacing.FRONT) {
+ Log.i(TAG, "Compensating rotation for front camera");
+ cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
+ } else {
+ cwNeededRotation = cwRotationFromDisplayToCamera;
+ }
+ Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);
+
+ Point theScreenResolution = new Point();
+ display.getSize(theScreenResolution);
+ screenResolution = theScreenResolution;
+ Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
+ cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
+ Log.i(TAG, "Camera resolution: " + cameraResolution);
+ bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
+ Log.i(TAG, "Best available preview size: " + bestPreviewSize);
+
+ boolean isScreenPortrait = screenResolution.x < screenResolution.y;
+ boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;
+
+ if (isScreenPortrait == isPreviewSizePortrait) {
+ previewSizeOnScreen = bestPreviewSize;
+ } else {
+ previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
+ }
+ Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);
+ }
+
+ void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) {
+
+ Camera theCamera = camera.getCamera();
+ Camera.Parameters parameters = theCamera.getParameters();
+
+ if (parameters == null) {
+ Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
+ return;
+ }
+
+ Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
+
+ if (safeMode) {
+ Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ initializeTorch(parameters, prefs, safeMode);
+
+ CameraConfigurationUtils.setFocus(
+ parameters,
+ prefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true),
+ prefs.getBoolean(PreferencesActivity.KEY_DISABLE_CONTINUOUS_FOCUS, true),
+ safeMode);
+
+ if (!safeMode) {
+ if (prefs.getBoolean(PreferencesActivity.KEY_INVERT_SCAN, false)) {
+ CameraConfigurationUtils.setInvertColor(parameters);
+ }
+
+ if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_BARCODE_SCENE_MODE, true)) {
+ CameraConfigurationUtils.setBarcodeSceneMode(parameters);
+ }
+
+ if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_METERING, true)) {
+ CameraConfigurationUtils.setVideoStabilization(parameters);
+ CameraConfigurationUtils.setFocusArea(parameters);
+ CameraConfigurationUtils.setMetering(parameters);
+ }
+
+ }
+
+ parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);
+
+ theCamera.setParameters(parameters);
+
+ theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera);
+
+ Camera.Parameters afterParameters = theCamera.getParameters();
+ Camera.Size afterSize = afterParameters.getPreviewSize();
+ if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) {
+ Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y +
+ ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
+ bestPreviewSize.x = afterSize.width;
+ bestPreviewSize.y = afterSize.height;
+ }
+ }
+
+ Point getBestPreviewSize() {
+ return bestPreviewSize;
+ }
+
+ Point getPreviewSizeOnScreen() {
+ return previewSizeOnScreen;
+ }
+
+ Point getCameraResolution() {
+ return cameraResolution;
+ }
+
+ Point getScreenResolution() {
+ return screenResolution;
+ }
+
+ int getCWNeededRotation() {
+ return cwNeededRotation;
+ }
+
+ boolean getTorchState(Camera camera) {
+ if (camera != null) {
+ Camera.Parameters parameters = camera.getParameters();
+ if (parameters != null) {
+ String flashMode = camera.getParameters().getFlashMode();
+ return flashMode != null &&
+ (Camera.Parameters.FLASH_MODE_ON.equals(flashMode) ||
+ Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode));
+ }
+ }
+ return false;
+ }
+
+ void setTorch(Camera camera, boolean newSetting) {
+ Camera.Parameters parameters = camera.getParameters();
+ doSetTorch(parameters, newSetting, false);
+ camera.setParameters(parameters);
+ }
+
+ private void initializeTorch(Camera.Parameters parameters, SharedPreferences prefs, boolean safeMode) {
+ boolean currentSetting = FrontLightMode.readPref(prefs) == FrontLightMode.ON;
+ doSetTorch(parameters, currentSetting, safeMode);
+ }
+
+ private void doSetTorch(Camera.Parameters parameters, boolean newSetting, boolean safeMode) {
+ CameraConfigurationUtils.setTorch(parameters, newSetting);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!safeMode && !prefs.getBoolean(PreferencesActivity.KEY_DISABLE_EXPOSURE, true)) {
+ CameraConfigurationUtils.setBestExposure(parameters, newSetting);
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java b/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java
new file mode 100644
index 0000000..9e5478f
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/CameraManager.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2008 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.camera;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.client.android.camera.open.OpenCamera;
+import com.google.zxing.client.android.camera.open.OpenCameraInterface;
+
+import java.io.IOException;
+
+/**
+ * This object wraps the Camera service object and expects to be the only one talking to it. The
+ * implementation encapsulates the steps needed to take preview-sized images, which are used for
+ * both preview and decoding.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class CameraManager {
+
+ private static final String TAG = CameraManager.class.getSimpleName();
+
+ private static final int MIN_FRAME_WIDTH = 240;
+ private static final int MIN_FRAME_HEIGHT = 240;
+ private static final int MAX_FRAME_WIDTH = 1200; // = 5/8 * 1920
+ private static final int MAX_FRAME_HEIGHT = 675; // = 5/8 * 1080
+
+ private final Context context;
+ private final CameraConfigurationManager configManager;
+ private OpenCamera camera;
+ private AutoFocusManager autoFocusManager;
+ private Rect framingRect;
+ private Rect framingRectInPreview;
+ private boolean initialized;
+ private boolean previewing;
+ private int requestedCameraId = OpenCameraInterface.NO_REQUESTED_CAMERA;
+ private int requestedFramingRectWidth;
+ private int requestedFramingRectHeight;
+ /**
+ * Preview frames are delivered here, which we pass on to the registered handler. Make sure to
+ * clear the handler so it will only receive one message.
+ */
+ private final PreviewCallback previewCallback;
+
+ public CameraManager(Context context) {
+ this.context = context;
+ this.configManager = new CameraConfigurationManager(context);
+ previewCallback = new PreviewCallback(configManager);
+ }
+
+ /**
+ * Opens the camera driver and initializes the hardware parameters.
+ *
+ * @param holder The surface object which the camera will draw preview frames into.
+ * @throws IOException Indicates the camera driver failed to open.
+ */
+ public synchronized void openDriver(SurfaceHolder holder) throws IOException {
+ OpenCamera theCamera = camera;
+ if (theCamera == null) {
+ theCamera = OpenCameraInterface.open(requestedCameraId);
+ if (theCamera == null) {
+ throw new IOException("Camera.open() failed to return object from driver");
+ }
+ camera = theCamera;
+ }
+
+ if (!initialized) {
+ initialized = true;
+ configManager.initFromCameraParameters(theCamera);
+ if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
+ setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
+ requestedFramingRectWidth = 0;
+ requestedFramingRectHeight = 0;
+ }
+ }
+
+ Camera cameraObject = theCamera.getCamera();
+ Camera.Parameters parameters = cameraObject.getParameters();
+ String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
+ try {
+ configManager.setDesiredCameraParameters(theCamera, false);
+ } catch (RuntimeException re) {
+ // Driver failed
+ Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
+ Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
+ // Reset:
+ if (parametersFlattened != null) {
+ parameters = cameraObject.getParameters();
+ parameters.unflatten(parametersFlattened);
+ try {
+ cameraObject.setParameters(parameters);
+ configManager.setDesiredCameraParameters(theCamera, true);
+ } catch (RuntimeException re2) {
+ // Well, darn. Give up
+ Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
+ }
+ }
+ }
+ cameraObject.setPreviewDisplay(holder);
+
+ }
+
+ public synchronized boolean isOpen() {
+ return camera != null;
+ }
+
+ /**
+ * Closes the camera driver if still in use.
+ */
+ public synchronized void closeDriver() {
+ if (camera != null) {
+ camera.getCamera().release();
+ camera = null;
+ // Make sure to clear these each time we close the camera, so that any scanning rect
+ // requested by intent is forgotten.
+ framingRect = null;
+ framingRectInPreview = null;
+ }
+ }
+
+ /**
+ * Asks the camera hardware to begin drawing preview frames to the screen.
+ */
+ public synchronized void startPreview() {
+ OpenCamera theCamera = camera;
+ if (theCamera != null && !previewing) {
+ theCamera.getCamera().startPreview();
+ previewing = true;
+ autoFocusManager = new AutoFocusManager(context, theCamera.getCamera());
+ }
+ }
+
+ /**
+ * Tells the camera to stop drawing preview frames.
+ */
+ public synchronized void stopPreview() {
+ if (autoFocusManager != null) {
+ autoFocusManager.stop();
+ autoFocusManager = null;
+ }
+ if (camera != null && previewing) {
+ camera.getCamera().stopPreview();
+ previewCallback.setHandler(null, 0);
+ previewing = false;
+ }
+ }
+
+ /**
+ * Convenience method for {@link com.google.zxing.client.android.CaptureActivity}
+ *
+ * @param newSetting if {@code true}, light should be turned on if currently off. And vice versa.
+ */
+ public synchronized void setTorch(boolean newSetting) {
+ OpenCamera theCamera = camera;
+ if (theCamera != null) {
+ if (newSetting != configManager.getTorchState(theCamera.getCamera())) {
+ boolean wasAutoFocusManager = autoFocusManager != null;
+ if (wasAutoFocusManager) {
+ autoFocusManager.stop();
+ autoFocusManager = null;
+ }
+ configManager.setTorch(theCamera.getCamera(), newSetting);
+ if (wasAutoFocusManager) {
+ autoFocusManager = new AutoFocusManager(context, theCamera.getCamera());
+ autoFocusManager.start();
+ }
+ }
+ }
+ }
+
+ /**
+ * A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
+ * in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
+ * respectively.
+ *
+ * @param handler The handler to send the message to.
+ * @param message The what field of the message to be sent.
+ */
+ public synchronized void requestPreviewFrame(Handler handler, int message) {
+ OpenCamera theCamera = camera;
+ if (theCamera != null && previewing) {
+ previewCallback.setHandler(handler, message);
+ theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
+ }
+ }
+
+ /**
+ * Calculates the framing rect which the UI should draw to show the user where to place the
+ * barcode. This target helps with alignment as well as forces the user to hold the device
+ * far enough away to ensure the image will be in focus.
+ *
+ * @return The rectangle to draw on screen in window coordinates.
+ */
+ public synchronized Rect getFramingRect() {
+ if (framingRect == null) {
+ if (camera == null) {
+ return null;
+ }
+ Point screenResolution = configManager.getScreenResolution();
+ if (screenResolution == null) {
+ // Called early, before init even finished
+ return null;
+ }
+
+ int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
+ int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);
+
+ int leftOffset = (screenResolution.x - width) / 2;
+ int topOffset = (screenResolution.y - height) / 2;
+ framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
+ Log.d(TAG, "Calculated framing rect: " + framingRect);
+ }
+ return framingRect;
+ }
+
+ private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) {
+ int dim = 5 * resolution / 8; // Target 5/8 of each dimension
+ if (dim < hardMin) {
+ return hardMin;
+ }
+ if (dim > hardMax) {
+ return hardMax;
+ }
+ return dim;
+ }
+
+ /**
+ * Like {@link #getFramingRect} but coordinates are in terms of the preview frame,
+ * not UI / screen.
+ *
+ * @return {@link Rect} expressing barcode scan area in terms of the preview size
+ */
+ public synchronized Rect getFramingRectInPreview() {
+ if (framingRectInPreview == null) {
+ Rect framingRect = getFramingRect();
+ if (framingRect == null) {
+ return null;
+ }
+ Rect rect = new Rect(framingRect);
+ Point cameraResolution = configManager.getCameraResolution();
+ Point screenResolution = configManager.getScreenResolution();
+ if (cameraResolution == null || screenResolution == null) {
+ // Called early, before init even finished
+ return null;
+ }
+ rect.left = rect.left * cameraResolution.x / screenResolution.x;
+ rect.right = rect.right * cameraResolution.x / screenResolution.x;
+ rect.top = rect.top * cameraResolution.y / screenResolution.y;
+ rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
+ framingRectInPreview = rect;
+ }
+ return framingRectInPreview;
+ }
+
+
+ /**
+ * Allows third party apps to specify the camera ID, rather than determine
+ * it automatically based on available cameras and their orientation.
+ *
+ * @param cameraId camera ID of the camera to use. A negative value means "no preference".
+ */
+ public synchronized void setManualCameraId(int cameraId) {
+ requestedCameraId = cameraId;
+ }
+
+ /**
+ * Allows third party apps to specify the scanning rectangle dimensions, rather than determine
+ * them automatically based on screen resolution.
+ *
+ * @param width The width in pixels to scan.
+ * @param height The height in pixels to scan.
+ */
+ public synchronized void setManualFramingRect(int width, int height) {
+ if (initialized) {
+ Point screenResolution = configManager.getScreenResolution();
+ if (width > screenResolution.x) {
+ width = screenResolution.x;
+ }
+ if (height > screenResolution.y) {
+ height = screenResolution.y;
+ }
+ int leftOffset = (screenResolution.x - width) / 2;
+ int topOffset = (screenResolution.y - height) / 2;
+ framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
+ Log.d(TAG, "Calculated manual framing rect: " + framingRect);
+ framingRectInPreview = null;
+ } else {
+ requestedFramingRectWidth = width;
+ requestedFramingRectHeight = height;
+ }
+ }
+
+ /**
+ * A factory method to build the appropriate LuminanceSource object based on the format
+ * of the preview buffers, as described by Camera.Parameters.
+ *
+ * @param data A preview frame.
+ * @param width The width of the image.
+ * @param height The height of the image.
+ * @return A PlanarYUVLuminanceSource instance.
+ */
+ public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
+ Rect rect = getFramingRectInPreview();
+ if (rect == null) {
+ return null;
+ }
+ // Go ahead and assume it's YUV rather than die.
+ return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
+ rect.width(), rect.height(), false);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java b/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java
new file mode 100644
index 0000000..3fbbb59
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/FrontLightMode.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.SharedPreferences;
+import com.google.zxing.client.android.PreferencesActivity;
+
+/**
+ * Enumerates settings of the preference controlling the front light.
+ */
+public enum FrontLightMode {
+
+ /** Always on. */
+ ON,
+ /** On only when ambient light is low. */
+ AUTO,
+ /** Always off. */
+ OFF;
+
+ private static FrontLightMode parse(String modeString) {
+ return modeString == null ? OFF : valueOf(modeString);
+ }
+
+ public static FrontLightMode readPref(SharedPreferences sharedPrefs) {
+ return parse(sharedPrefs.getString(PreferencesActivity.KEY_FRONT_LIGHT_MODE, OFF.toString()));
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java b/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java
new file mode 100644
index 0000000..7dc7035
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 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.camera;
+
+import android.graphics.Point;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+final class PreviewCallback implements Camera.PreviewCallback {
+
+ private static final String TAG = PreviewCallback.class.getSimpleName();
+
+ private final CameraConfigurationManager configManager;
+ private Handler previewHandler;
+ private int previewMessage;
+
+ PreviewCallback(CameraConfigurationManager configManager) {
+ this.configManager = configManager;
+ }
+
+ void setHandler(Handler previewHandler, int previewMessage) {
+ this.previewHandler = previewHandler;
+ this.previewMessage = previewMessage;
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ Point cameraResolution = configManager.getCameraResolution();
+ Handler thePreviewHandler = previewHandler;
+ if (cameraResolution != null && thePreviewHandler != null) {
+ Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
+ cameraResolution.y, data);
+ message.sendToTarget();
+ previewHandler = null;
+ } else {
+ Log.d(TAG, "Got preview callback, but no handler or resolution available");
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java b/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java
new file mode 100644
index 0000000..20fd4e3
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 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.camera.open;
+
+public enum CameraFacing {
+
+ BACK, // must be value 0!
+ FRONT, // must be value 1!
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java
new file mode 100644
index 0000000..ddac734
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.camera.open;
+
+import android.hardware.Camera;
+
+public final class OpenCamera {
+
+ private final int index;
+ private final Camera camera;
+ private final CameraFacing facing;
+ private final int orientation;
+
+ public OpenCamera(int index, Camera camera, CameraFacing facing, int orientation) {
+ this.index = index;
+ this.camera = camera;
+ this.facing = facing;
+ this.orientation = orientation;
+ }
+
+ public Camera getCamera() {
+ return camera;
+ }
+
+ public CameraFacing getFacing() {
+ return facing;
+ }
+
+ public int getOrientation() {
+ return orientation;
+ }
+
+ @Override
+ public String toString() {
+ return "Camera #" + index + " : " + facing + ',' + orientation;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java
new file mode 100644
index 0000000..24e0f13
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.camera.open;
+
+import android.hardware.Camera;
+import android.util.Log;
+
+public final class OpenCameraInterface {
+
+ private static final String TAG = OpenCameraInterface.class.getName();
+
+ private OpenCameraInterface() {
+ }
+
+ /** For {@link #open(int)}, means no preference for which camera to open. */
+ public static final int NO_REQUESTED_CAMERA = -1;
+
+ /**
+ * Opens the requested camera with {@link Camera#open(int)}, if one exists.
+ *
+ * @param cameraId camera ID of the camera to use. A negative value
+ * or {@link #NO_REQUESTED_CAMERA} means "no preference", in which case a rear-facing
+ * camera is returned if possible or else any camera
+ * @return handle to {@link OpenCamera} that was opened
+ */
+ public static OpenCamera open(int cameraId) {
+
+ int numCameras = Camera.getNumberOfCameras();
+ if (numCameras == 0) {
+ Log.w(TAG, "No cameras!");
+ return null;
+ }
+
+ boolean explicitRequest = cameraId >= 0;
+
+ Camera.CameraInfo selectedCameraInfo = null;
+ int index;
+ if (explicitRequest) {
+ index = cameraId;
+ selectedCameraInfo = new Camera.CameraInfo();
+ Camera.getCameraInfo(index, selectedCameraInfo);
+ } else {
+ index = 0;
+ while (index < numCameras) {
+ Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+ Camera.getCameraInfo(index, cameraInfo);
+ CameraFacing reportedFacing = CameraFacing.values()[cameraInfo.facing];
+ if (reportedFacing == CameraFacing.BACK) {
+ selectedCameraInfo = cameraInfo;
+ break;
+ }
+ index++;
+ }
+ }
+
+ Camera camera;
+ if (index < numCameras) {
+ Log.i(TAG, "Opening camera #" + index);
+ camera = Camera.open(index);
+ } else {
+ if (explicitRequest) {
+ Log.w(TAG, "Requested camera does not exist: " + cameraId);
+ camera = null;
+ } else {
+ Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0");
+ camera = Camera.open(0);
+ selectedCameraInfo = new Camera.CameraInfo();
+ Camera.getCameraInfo(0, selectedCameraInfo);
+ }
+ }
+
+ if (camera == null) {
+ return null;
+ }
+ return new OpenCamera(index,
+ camera,
+ CameraFacing.values()[selectedCameraInfo.facing],
+ selectedCameraInfo.orientation);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java b/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java
new file mode 100644
index 0000000..3ba0603
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/clipboard/ClipboardInterface.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.clipboard;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.util.Log;
+
+public final class ClipboardInterface {
+
+ private static final String TAG = ClipboardInterface.class.getSimpleName();
+
+ private ClipboardInterface() {
+ }
+
+ public static CharSequence getText(Context context) {
+ ClipboardManager clipboard = getManager(context);
+ ClipData clip = clipboard.getPrimaryClip();
+ return hasText(context) ? clip.getItemAt(0).coerceToText(context) : null;
+ }
+
+ public static void setText(CharSequence text, Context context) {
+ if (text != null) {
+ try {
+ getManager(context).setPrimaryClip(ClipData.newPlainText(null, text));
+ } catch (NullPointerException | IllegalStateException e) {
+ // Have seen this in the wild, bizarrely
+ Log.w(TAG, "Clipboard bug", e);
+ }
+ }
+ }
+
+ public static boolean hasText(Context context) {
+ ClipboardManager clipboard = getManager(context);
+ ClipData clip = clipboard.getPrimaryClip();
+ return clip != null && clip.getItemCount() > 0;
+ }
+
+ private static ClipboardManager getManager(Context context) {
+ return (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java
new file mode 100644
index 0000000..41639a9
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/ContactEncoder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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.encode;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Implementations encode according to some scheme for encoding contact information, like VCard or
+ * MECARD.
+ *
+ * @author Sean Owen
+ */
+abstract class ContactEncoder {
+
+ /**
+ * @return first, the best effort encoding of all data in the appropriate format; second, a
+ * display-appropriate version of the contact information
+ */
+ abstract String[] encode(List names,
+ String organization,
+ List addresses,
+ List phones,
+ List phoneTypes,
+ List emails,
+ List urls,
+ String note);
+
+ /**
+ * @return null if s is null or empty, or result of s.trim() otherwise
+ */
+ static String trim(String s) {
+ if (s == null) {
+ return null;
+ }
+ String result = s.trim();
+ return result.isEmpty() ? null : result;
+ }
+
+ static void append(StringBuilder newContents,
+ StringBuilder newDisplayContents,
+ String prefix,
+ String value,
+ Formatter fieldFormatter,
+ char terminator) {
+ String trimmed = trim(value);
+ if (trimmed != null) {
+ newContents.append(prefix).append(fieldFormatter.format(trimmed, 0)).append(terminator);
+ newDisplayContents.append(trimmed).append('\n');
+ }
+ }
+
+ static void appendUpToUnique(StringBuilder newContents,
+ StringBuilder newDisplayContents,
+ String prefix,
+ List values,
+ int max,
+ Formatter displayFormatter,
+ Formatter fieldFormatter,
+ char terminator) {
+ if (values == null) {
+ return;
+ }
+ int count = 0;
+ Collection uniques = new HashSet<>(2);
+ for (int i = 0; i < values.size(); i++) {
+ String value = values.get(i);
+ String trimmed = trim(value);
+ if (trimmed != null && !trimmed.isEmpty() && !uniques.contains(trimmed)) {
+ newContents.append(prefix).append(fieldFormatter.format(trimmed, i)).append(terminator);
+ CharSequence display = displayFormatter == null ? trimmed : displayFormatter.format(trimmed, i);
+ newDisplayContents.append(display).append('\n');
+ if (++count == max) {
+ break;
+ }
+ uniques.add(trimmed);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java b/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java
new file mode 100644
index 0000000..1165c52
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/EncodeActivity.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2008 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.encode;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.view.Display;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.google.zxing.WriterException;
+import com.google.zxing.client.android.Contents;
+import com.google.zxing.client.android.FinishListener;
+import com.google.zxing.client.android.Intents;
+
+import net.foucry.pilldroid.R;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+/**
+ * This class encodes data from an Intent into a QR code, and then displays it full screen so that
+ * another person can scan it with their device.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class EncodeActivity extends Activity {
+
+ private static final String TAG = EncodeActivity.class.getSimpleName();
+
+ private static final int MAX_BARCODE_FILENAME_LENGTH = 24;
+ private static final Pattern NOT_ALPHANUMERIC = Pattern.compile("[^A-Za-z0-9]");
+ private static final String USE_VCARD_KEY = "USE_VCARD";
+
+ private QRCodeEncoder qrCodeEncoder;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ Intent intent = getIntent();
+ if (intent == null) {
+ finish();
+ } else {
+ String action = intent.getAction();
+ if (Intents.Encode.ACTION.equals(action) || Intent.ACTION_SEND.equals(action)) {
+ setContentView(R.layout.encode);
+ } else {
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater menuInflater = getMenuInflater();
+ menuInflater.inflate(R.menu.encode, menu);
+ boolean useVcard = qrCodeEncoder != null && qrCodeEncoder.isUseVCard();
+ int encodeNameResource = useVcard ? R.string.menu_encode_mecard : R.string.menu_encode_vcard;
+ MenuItem encodeItem = menu.findItem(R.id.menu_encode);
+ encodeItem.setTitle(encodeNameResource);
+ Intent intent = getIntent();
+ if (intent != null) {
+ String type = intent.getStringExtra(Intents.Encode.TYPE);
+ encodeItem.setVisible(Contents.Type.CONTACT.equals(type));
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_share:
+ share();
+ return true;
+ case R.id.menu_encode:
+ Intent intent = getIntent();
+ if (intent == null) {
+ return false;
+ }
+ intent.putExtra(USE_VCARD_KEY, !qrCodeEncoder.isUseVCard());
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void share() {
+ QRCodeEncoder encoder = qrCodeEncoder;
+ if (encoder == null) { // Odd
+ Log.w(TAG, "No existing barcode to send?");
+ return;
+ }
+
+ String contents = encoder.getContents();
+ if (contents == null) {
+ Log.w(TAG, "No existing barcode to send?");
+ return;
+ }
+
+ Bitmap bitmap;
+ try {
+ bitmap = encoder.encodeAsBitmap();
+ } catch (WriterException we) {
+ Log.w(TAG, we);
+ return;
+ }
+ if (bitmap == null) {
+ return;
+ }
+
+ File bsRoot = new File(Environment.getExternalStorageDirectory(), "BarcodeScanner");
+ File barcodesRoot = new File(bsRoot, "Barcodes");
+ if (!barcodesRoot.exists() && !barcodesRoot.mkdirs()) {
+ Log.w(TAG, "Couldn't make dir " + barcodesRoot);
+ showErrorMessage(R.string.msg_unmount_usb);
+ return;
+ }
+ File barcodeFile = new File(barcodesRoot, makeBarcodeFileName(contents) + ".png");
+ if (!barcodeFile.delete()) {
+ Log.w(TAG, "Could not delete " + barcodeFile);
+ // continue anyway
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(barcodeFile);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 0, fos);
+ } catch (FileNotFoundException fnfe) {
+ Log.w(TAG, "Couldn't access file " + barcodeFile + " due to " + fnfe);
+ showErrorMessage(R.string.msg_unmount_usb);
+ return;
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException ioe) {
+ // do nothing
+ }
+ }
+ }
+
+ Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:"));
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " - " + encoder.getTitle());
+ intent.putExtra(Intent.EXTRA_TEXT, contents);
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + barcodeFile.getAbsolutePath()));
+ intent.setType("image/png");
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ startActivity(Intent.createChooser(intent, null));
+ }
+
+ private static CharSequence makeBarcodeFileName(CharSequence contents) {
+ String fileName = NOT_ALPHANUMERIC.matcher(contents).replaceAll("_");
+ if (fileName.length() > MAX_BARCODE_FILENAME_LENGTH) {
+ fileName = fileName.substring(0, MAX_BARCODE_FILENAME_LENGTH);
+ }
+ return fileName;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // This assumes the view is full screen, which is a good assumption
+ WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ Display display = manager.getDefaultDisplay();
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+ int width = displaySize.x;
+ int height = displaySize.y;
+ int smallerDimension = width < height ? width : height;
+ smallerDimension = smallerDimension * 7 / 8;
+
+ Intent intent = getIntent();
+ if (intent == null) {
+ return;
+ }
+
+ try {
+ boolean useVCard = intent.getBooleanExtra(USE_VCARD_KEY, false);
+ qrCodeEncoder = new QRCodeEncoder(this, intent, smallerDimension, useVCard);
+ Bitmap bitmap = qrCodeEncoder.encodeAsBitmap();
+ if (bitmap == null) {
+ Log.w(TAG, "Could not encode barcode");
+ showErrorMessage(R.string.msg_encode_contents_failed);
+ qrCodeEncoder = null;
+ return;
+ }
+
+ ImageView view = (ImageView) findViewById(R.id.image_view);
+ view.setImageBitmap(bitmap);
+
+ TextView contents = (TextView) findViewById(R.id.contents_text_view);
+ if (intent.getBooleanExtra(Intents.Encode.SHOW_CONTENTS, true)) {
+ contents.setText(qrCodeEncoder.getDisplayContents());
+ setTitle(qrCodeEncoder.getTitle());
+ } else {
+ contents.setText("");
+ setTitle("");
+ }
+ } catch (WriterException e) {
+ Log.w(TAG, "Could not encode barcode", e);
+ showErrorMessage(R.string.msg_encode_contents_failed);
+ qrCodeEncoder = null;
+ }
+ }
+
+ private void showErrorMessage(int message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(message);
+ builder.setPositiveButton(R.string.button_ok, new FinishListener(this));
+ builder.setOnCancelListener(new FinishListener(this));
+ builder.show();
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java b/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java
new file mode 100644
index 0000000..9e0ae85
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/Formatter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 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.encode;
+
+/**
+ * Encapsulates some simple formatting logic, to aid refactoring in {@link ContactEncoder}.
+ *
+ * @author Sean Owen
+ */
+interface Formatter {
+
+ /**
+ * @param value value to format
+ * @param index index of value in a list of values to be formatted
+ * @return formatted value
+ */
+ CharSequence format(CharSequence value, int index);
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java
new file mode 100644
index 0000000..f41d854
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/MECARDContactEncoder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2011 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.encode;
+
+import android.telephony.PhoneNumberUtils;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Encodes contact information according to the MECARD format.
+ *
+ * @author Sean Owen
+ */
+final class MECARDContactEncoder extends ContactEncoder {
+
+ private static final char TERMINATOR = ';';
+
+ @Override
+ public String[] encode(List names,
+ String organization,
+ List addresses,
+ List phones,
+ List phoneTypes,
+ List emails,
+ List urls,
+ String note) {
+ StringBuilder newContents = new StringBuilder(100);
+ newContents.append("MECARD:");
+
+ StringBuilder newDisplayContents = new StringBuilder(100);
+
+ Formatter fieldFormatter = new MECARDFieldFormatter();
+
+ appendUpToUnique(newContents, newDisplayContents, "N", names, 1, new
+ MECARDNameDisplayFormatter(), fieldFormatter, TERMINATOR);
+
+ append(newContents, newDisplayContents, "ORG", organization, fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "ADR", addresses, 1, null, fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "TEL", phones, Integer.MAX_VALUE,
+ new MECARDTelDisplayFormatter(), fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "EMAIL", emails, Integer.MAX_VALUE, null,
+ fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "URL", urls, Integer.MAX_VALUE, null,
+ fieldFormatter, TERMINATOR);
+
+ append(newContents, newDisplayContents, "NOTE", note, fieldFormatter, TERMINATOR);
+
+ newContents.append(';');
+
+ return new String[] { newContents.toString(), newDisplayContents.toString() };
+ }
+
+ private static class MECARDFieldFormatter implements Formatter {
+ private static final Pattern RESERVED_MECARD_CHARS = Pattern.compile("([\\\\:;])");
+ private static final Pattern NEWLINE = Pattern.compile("\\n");
+ @Override
+ public CharSequence format(CharSequence value, int index) {
+ return ':' + NEWLINE.matcher(RESERVED_MECARD_CHARS.matcher(value).replaceAll("\\\\$1")).replaceAll("");
+ }
+ }
+
+ private static class MECARDTelDisplayFormatter implements Formatter {
+ private static final Pattern NOT_DIGITS = Pattern.compile("[^0-9]+");
+ @Override
+ public CharSequence format(CharSequence value, int index) {
+ return NOT_DIGITS.matcher(PhoneNumberUtils.formatNumber(value.toString())).replaceAll("");
+ }
+ }
+
+ private static class MECARDNameDisplayFormatter implements Formatter {
+ private static final Pattern COMMA = Pattern.compile(",");
+ @Override
+ public CharSequence format(CharSequence value, int index) {
+ return COMMA.matcher(value).replaceAll("");
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java
new file mode 100644
index 0000000..3bc98db
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/QRCodeEncoder.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2008 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.encode;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+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.Contents;
+import com.google.zxing.client.android.Intents;
+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 net.foucry.pilldroid.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class does the work of decoding the user's request and extracting all the data
+ * 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 Context activity;
+ private String contents;
+ private String displayContents;
+ private String title;
+ private BarcodeFormat format;
+ private final int dimension;
+ private final boolean useVCard;
+
+ QRCodeEncoder(Context activity, Intent intent, int dimension, boolean useVCard) throws WriterException {
+ this.activity = activity;
+ this.dimension = dimension;
+ this.useVCard = useVCard;
+ String action = intent.getAction();
+ if (Intents.Encode.ACTION.equals(action)) {
+ encodeContentsFromZXingIntent(intent);
+ } else if (Intent.ACTION_SEND.equals(action)) {
+ encodeContentsFromShareIntent(intent);
+ }
+ }
+
+ String getContents() {
+ return contents;
+ }
+
+ String getDisplayContents() {
+ return displayContents;
+ }
+
+ String getTitle() {
+ return title;
+ }
+
+ boolean isUseVCard() {
+ return useVCard;
+ }
+
+ // 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 encodeContentsFromZXingIntent(Intent intent) {
+ // Default to QR_CODE if no format given.
+ String formatString = intent.getStringExtra(Intents.Encode.FORMAT);
+ format = null;
+ if (formatString != null) {
+ try {
+ format = BarcodeFormat.valueOf(formatString);
+ } catch (IllegalArgumentException iae) {
+ // Ignore it then
+ }
+ }
+ if (format == null || format == BarcodeFormat.QR_CODE) {
+ String type = intent.getStringExtra(Intents.Encode.TYPE);
+ if (type == null || type.isEmpty()) {
+ return false;
+ }
+ this.format = BarcodeFormat.QR_CODE;
+ encodeQRCodeContents(intent, type);
+ } else {
+ String data = intent.getStringExtra(Intents.Encode.DATA);
+ if (data != null && !data.isEmpty()) {
+ contents = data;
+ displayContents = data;
+ title = activity.getString(R.string.contents_text);
+ }
+ }
+ return contents != null && !contents.isEmpty();
+ }
+
+ // Handles send intents from multitude of Android applications
+ private void encodeContentsFromShareIntent(Intent intent) throws WriterException {
+ // Check if this is a plain text encoding, or contact
+ if (intent.hasExtra(Intent.EXTRA_STREAM)) {
+ encodeFromStreamExtra(intent);
+ } else {
+ encodeFromTextExtras(intent);
+ }
+ }
+
+ private void encodeFromTextExtras(Intent intent) throws WriterException {
+ // Notice: Google Maps shares both URL and details in one text, bummer!
+ String theContents = ContactEncoder.trim(intent.getStringExtra(Intent.EXTRA_TEXT));
+ if (theContents == null) {
+ theContents = ContactEncoder.trim(intent.getStringExtra("android.intent.extra.HTML_TEXT"));
+ // Intent.EXTRA_HTML_TEXT
+ if (theContents == null) {
+ theContents = ContactEncoder.trim(intent.getStringExtra(Intent.EXTRA_SUBJECT));
+ if (theContents == null) {
+ String[] emails = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
+ if (emails != null) {
+ theContents = ContactEncoder.trim(emails[0]);
+ } else {
+ theContents = "?";
+ }
+ }
+ }
+ }
+
+ // Trim text to avoid URL breaking.
+ if (theContents == null || theContents.isEmpty()) {
+ throw new WriterException("Empty EXTRA_TEXT");
+ }
+ contents = theContents;
+ // We only do QR code.
+ format = BarcodeFormat.QR_CODE;
+ if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
+ displayContents = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ } else if (intent.hasExtra(Intent.EXTRA_TITLE)) {
+ displayContents = intent.getStringExtra(Intent.EXTRA_TITLE);
+ } else {
+ displayContents = contents;
+ }
+ title = activity.getString(R.string.contents_text);
+ }
+
+ // Handles send intents from the Contacts app, retrieving a contact as a VCARD.
+ private void encodeFromStreamExtra(Intent intent) throws WriterException {
+ format = BarcodeFormat.QR_CODE;
+ Bundle bundle = intent.getExtras();
+ if (bundle == null) {
+ throw new WriterException("No extras");
+ }
+ Uri uri = bundle.getParcelable(Intent.EXTRA_STREAM);
+ if (uri == null) {
+ throw new WriterException("No EXTRA_STREAM");
+ }
+ byte[] vcard;
+ String vcardString;
+ InputStream stream = null;
+ try {
+ stream = activity.getContentResolver().openInputStream(uri);
+ if (stream == null) {
+ throw new WriterException("Can't open stream for " + uri);
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[2048];
+ int bytesRead;
+ while ((bytesRead = stream.read(buffer)) > 0) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ vcard = baos.toByteArray();
+ vcardString = new String(vcard, 0, vcard.length, "UTF-8");
+ } catch (IOException ioe) {
+ throw new WriterException(ioe);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // continue
+ }
+ }
+ }
+ 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)) {
+ throw new WriterException("Result was not an address");
+ }
+ encodeQRCodeContents((AddressBookParsedResult) parsedResult);
+ if (contents == null || contents.isEmpty()) {
+ throw new WriterException("No content to encode");
+ }
+ }
+
+ private void encodeQRCodeContents(Intent intent, String type) {
+ switch (type) {
+ case Contents.Type.TEXT:
+ String textData = intent.getStringExtra(Intents.Encode.DATA);
+ if (textData != null && !textData.isEmpty()) {
+ contents = textData;
+ displayContents = textData;
+ title = activity.getString(R.string.contents_text);
+ }
+ break;
+
+ case Contents.Type.EMAIL:
+ String emailData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA));
+ if (emailData != null) {
+ contents = "mailto:" + emailData;
+ displayContents = emailData;
+ title = activity.getString(R.string.contents_email);
+ }
+ break;
+
+ case Contents.Type.PHONE:
+ String phoneData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA));
+ if (phoneData != null) {
+ contents = "tel:" + phoneData;
+ displayContents = PhoneNumberUtils.formatNumber(phoneData);
+ title = activity.getString(R.string.contents_phone);
+ }
+ break;
+
+ case Contents.Type.SMS:
+ String smsData = ContactEncoder.trim(intent.getStringExtra(Intents.Encode.DATA));
+ if (smsData != null) {
+ contents = "sms:" + smsData;
+ displayContents = PhoneNumberUtils.formatNumber(smsData);
+ title = activity.getString(R.string.contents_sms);
+ }
+ break;
+
+ case Contents.Type.CONTACT:
+ Bundle contactBundle = intent.getBundleExtra(Intents.Encode.DATA);
+ if (contactBundle != null) {
+
+ String name = contactBundle.getString(ContactsContract.Intents.Insert.NAME);
+ String organization = contactBundle.getString(ContactsContract.Intents.Insert.COMPANY);
+ String address = contactBundle.getString(ContactsContract.Intents.Insert.POSTAL);
+ List phones = getAllBundleValues(contactBundle, Contents.PHONE_KEYS);
+ List phoneTypes = getAllBundleValues(contactBundle, Contents.PHONE_TYPE_KEYS);
+ List emails = getAllBundleValues(contactBundle, Contents.EMAIL_KEYS);
+ String url = contactBundle.getString(Contents.URL_KEY);
+ List urls = url == null ? null : Collections.singletonList(url);
+ String note = contactBundle.getString(Contents.NOTE_KEY);
+
+ ContactEncoder encoder = useVCard ? new VCardContactEncoder() : new MECARDContactEncoder();
+ String[] encoded = encoder.encode(Collections.singletonList(name),
+ organization,
+ Collections.singletonList(address),
+ phones,
+ phoneTypes,
+ emails,
+ urls,
+ note);
+ // Make sure we've encoded at least one field.
+ if (!encoded[1].isEmpty()) {
+ contents = encoded[0];
+ displayContents = encoded[1];
+ title = activity.getString(R.string.contents_contact);
+ }
+
+ }
+ break;
+
+ case Contents.Type.LOCATION:
+ Bundle locationBundle = intent.getBundleExtra(Intents.Encode.DATA);
+ if (locationBundle != null) {
+ // These must use Bundle.getFloat(), not getDouble(), it's part of the API.
+ float latitude = locationBundle.getFloat("LAT", Float.MAX_VALUE);
+ float longitude = locationBundle.getFloat("LONG", Float.MAX_VALUE);
+ if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) {
+ contents = "geo:" + latitude + ',' + longitude;
+ displayContents = latitude + "," + longitude;
+ title = activity.getString(R.string.contents_location);
+ }
+ }
+ break;
+ }
+ }
+
+ private static List getAllBundleValues(Bundle bundle, String[] keys) {
+ List values = new ArrayList<>(keys.length);
+ for (String key : keys) {
+ Object value = bundle.get(key);
+ values.add(value == null ? null : value.toString());
+ }
+ return values;
+ }
+
+ private void encodeQRCodeContents(AddressBookParsedResult contact) {
+ ContactEncoder encoder = useVCard ? new VCardContactEncoder() : new MECARDContactEncoder();
+ String[] encoded = encoder.encode(toList(contact.getNames()),
+ contact.getOrg(),
+ toList(contact.getAddresses()),
+ toList(contact.getPhoneNumbers()),
+ null,
+ toList(contact.getEmails()),
+ toList(contact.getURLs()),
+ null);
+ // Make sure we've encoded at least one field.
+ if (!encoded[1].isEmpty()) {
+ contents = encoded[0];
+ displayContents = encoded[1];
+ title = activity.getString(R.string.contents_contact);
+ }
+ }
+
+ private static List toList(String[] values) {
+ return values == null ? null : Arrays.asList(values);
+ }
+
+ Bitmap encodeAsBitmap() throws WriterException {
+ String contentsToEncode = contents;
+ if (contentsToEncode == null) {
+ return null;
+ }
+ Map hints = null;
+ String encoding = guessAppropriateEncoding(contentsToEncode);
+ if (encoding != null) {
+ hints = new EnumMap<>(EncodeHintType.class);
+ hints.put(EncodeHintType.CHARACTER_SET, encoding);
+ }
+ BitMatrix result;
+ try {
+ result = new MultiFormatWriter().encode(contentsToEncode, format, dimension, dimension, hints);
+ } catch (IllegalArgumentException iae) {
+ // Unsupported format
+ return null;
+ }
+ int width = result.getWidth();
+ int height = result.getHeight();
+ int[] pixels = new int[width * height];
+ 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;
+ }
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
+ return bitmap;
+ }
+
+ 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;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java
new file mode 100644
index 0000000..56ced33
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardContactEncoder.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2011 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.encode;
+
+import android.provider.ContactsContract;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Encodes contact information according to the vCard format.
+ *
+ * @author Sean Owen
+ */
+final class VCardContactEncoder extends ContactEncoder {
+
+ private static final char TERMINATOR = '\n';
+
+ @Override
+ public String[] encode(List names,
+ String organization,
+ List addresses,
+ List phones,
+ List phoneTypes,
+ List emails,
+ List urls,
+ String note) {
+ StringBuilder newContents = new StringBuilder(100);
+ newContents.append("BEGIN:VCARD").append(TERMINATOR);
+ newContents.append("VERSION:3.0").append(TERMINATOR);
+
+ StringBuilder newDisplayContents = new StringBuilder(100);
+
+ Formatter fieldFormatter = new VCardFieldFormatter();
+
+ appendUpToUnique(newContents, newDisplayContents, "N", names, 1, null, fieldFormatter, TERMINATOR);
+
+ append(newContents, newDisplayContents, "ORG", organization, fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "ADR", addresses, 1, null, fieldFormatter, TERMINATOR);
+
+ List>> phoneMetadata = buildPhoneMetadata(phones, phoneTypes);
+ appendUpToUnique(newContents, newDisplayContents, "TEL", phones, Integer.MAX_VALUE,
+ new VCardTelDisplayFormatter(phoneMetadata),
+ new VCardFieldFormatter(phoneMetadata), TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "EMAIL", emails, Integer.MAX_VALUE, null,
+ fieldFormatter, TERMINATOR);
+
+ appendUpToUnique(newContents, newDisplayContents, "URL", urls, Integer.MAX_VALUE, null,
+ fieldFormatter, TERMINATOR);
+
+ append(newContents, newDisplayContents, "NOTE", note, fieldFormatter, TERMINATOR);
+
+ newContents.append("END:VCARD").append(TERMINATOR);
+
+ return new String[] { newContents.toString(), newDisplayContents.toString() };
+ }
+
+ static List>> buildPhoneMetadata(Collection phones, List phoneTypes) {
+ if (phoneTypes == null || phoneTypes.isEmpty()) {
+ return null;
+ }
+ List>> metadataForIndex = new ArrayList<>();
+ for (int i = 0; i < phones.size(); i++) {
+ if (phoneTypes.size() <= i) {
+ metadataForIndex.add(null);
+ } else {
+ Map> metadata = new HashMap<>();
+ metadataForIndex.add(metadata);
+ Set typeTokens = new HashSet<>();
+ metadata.put("TYPE", typeTokens);
+ String typeString = phoneTypes.get(i);
+ Integer androidType = maybeIntValue(typeString);
+ if (androidType == null) {
+ typeTokens.add(typeString);
+ } else {
+ String purpose = vCardPurposeLabelForAndroidType(androidType);
+ String context = vCardContextLabelForAndroidType(androidType);
+ if (purpose != null) {
+ typeTokens.add(purpose);
+ }
+ if (context != null) {
+ typeTokens.add(context);
+ }
+ }
+ }
+ }
+ return metadataForIndex;
+ }
+
+ private static Integer maybeIntValue(String value) {
+ try {
+ return Integer.valueOf(value);
+ } catch (NumberFormatException nfe) {
+ return null;
+ }
+ }
+
+ private static String vCardPurposeLabelForAndroidType(int androidType) {
+ switch (androidType) {
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX:
+ return "fax";
+ case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER:
+ return "pager";
+ case ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD:
+ return "textphone";
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MMS:
+ return "text";
+ default:
+ return null;
+ }
+ }
+
+ private static String vCardContextLabelForAndroidType(int androidType) {
+ switch (androidType) {
+ case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER:
+ return "home";
+ case ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK:
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER:
+ return "work";
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java
new file mode 100644
index 0000000..070a4cd
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardFieldFormatter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 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.encode;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * @author Sean Owen
+ */
+final class VCardFieldFormatter implements Formatter {
+
+ private static final Pattern RESERVED_VCARD_CHARS = Pattern.compile("([\\\\,;])");
+ private static final Pattern NEWLINE = Pattern.compile("\\n");
+
+ private final List>> metadataForIndex;
+
+ VCardFieldFormatter() {
+ this(null);
+ }
+
+ VCardFieldFormatter(List>> metadataForIndex) {
+ this.metadataForIndex = metadataForIndex;
+ }
+
+ @Override
+ public CharSequence format(CharSequence value, int index) {
+ value = RESERVED_VCARD_CHARS.matcher(value).replaceAll("\\\\$1");
+ value = NEWLINE.matcher(value).replaceAll("");
+ Map> metadata =
+ metadataForIndex == null || metadataForIndex.size() <= index ? null : metadataForIndex.get(index);
+ value = formatMetadata(value, metadata);
+ return value;
+ }
+
+ private static CharSequence formatMetadata(CharSequence value, Map> metadata) {
+ StringBuilder withMetadata = new StringBuilder();
+ if (metadata != null) {
+ for (Map.Entry> metadatum : metadata.entrySet()) {
+ Set values = metadatum.getValue();
+ if (values == null || values.isEmpty()) {
+ continue;
+ }
+ withMetadata.append(';').append(metadatum.getKey()).append('=');
+ if (values.size() > 1) {
+ withMetadata.append('"');
+ }
+ Iterator valuesIt = values.iterator();
+ withMetadata.append(valuesIt.next());
+ while (valuesIt.hasNext()) {
+ withMetadata.append(',').append(valuesIt.next());
+ }
+ if (values.size() > 1) {
+ withMetadata.append('"');
+ }
+ }
+ }
+ withMetadata.append(':').append(value);
+ return withMetadata;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java b/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java
new file mode 100644
index 0000000..f5eec9b
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/encode/VCardTelDisplayFormatter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014 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.encode;
+
+import android.telephony.PhoneNumberUtils;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Sean Owen
+ */
+final class VCardTelDisplayFormatter implements Formatter {
+
+ private final List>> metadataForIndex;
+
+ VCardTelDisplayFormatter() {
+ this(null);
+ }
+
+ VCardTelDisplayFormatter(List>> metadataForIndex) {
+ this.metadataForIndex = metadataForIndex;
+ }
+
+ @Override
+ public CharSequence format(CharSequence value, int index) {
+ value = PhoneNumberUtils.formatNumber(value.toString());
+ Map> metadata =
+ metadataForIndex == null || metadataForIndex.size() <= index ? null : metadataForIndex.get(index);
+ value = formatMetadata(value, metadata);
+ return value;
+ }
+
+ private static CharSequence formatMetadata(CharSequence value, Map> metadata) {
+ if (metadata == null || metadata.isEmpty()) {
+ return value;
+ }
+ StringBuilder withMetadata = new StringBuilder();
+ for (Map.Entry> metadatum : metadata.entrySet()) {
+ Set values = metadatum.getValue();
+ if (values == null || values.isEmpty()) {
+ continue;
+ }
+ Iterator valuesIt = values.iterator();
+ withMetadata.append(valuesIt.next());
+ while (valuesIt.hasNext()) {
+ withMetadata.append(',').append(valuesIt.next());
+ }
+ }
+ if (withMetadata.length() > 0) {
+ withMetadata.append(' ');
+ }
+ withMetadata.append(value);
+ return withMetadata;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java b/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java
new file mode 100644
index 0000000..41c40a5
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/history/DBHelper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 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.history;
+
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteDatabase;
+import android.content.Context;
+
+/**
+ * @author Sean Owen
+ */
+final class DBHelper extends SQLiteOpenHelper {
+
+ private static final int DB_VERSION = 5;
+ private static final String DB_NAME = "barcode_scanner_history.db";
+ static final String TABLE_NAME = "history";
+ static final String ID_COL = "id";
+ static final String TEXT_COL = "text";
+ static final String FORMAT_COL = "format";
+ static final String DISPLAY_COL = "display";
+ static final String TIMESTAMP_COL = "timestamp";
+ static final String DETAILS_COL = "details";
+
+ DBHelper(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase sqLiteDatabase) {
+ sqLiteDatabase.execSQL(
+ "CREATE TABLE " + TABLE_NAME + " (" +
+ ID_COL + " INTEGER PRIMARY KEY, " +
+ TEXT_COL + " TEXT, " +
+ FORMAT_COL + " TEXT, " +
+ DISPLAY_COL + " TEXT, " +
+ TIMESTAMP_COL + " INTEGER, " +
+ DETAILS_COL + " TEXT);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+ onCreate(sqLiteDatabase);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java
new file mode 100644
index 0000000..080af97
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryActivity.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2012 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.history;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import com.google.zxing.client.android.CaptureActivity;
+import com.google.zxing.client.android.Intents;
+
+import net.foucry.pilldroid.R;
+
+public final class HistoryActivity extends ListActivity {
+
+ private static final String TAG = HistoryActivity.class.getSimpleName();
+
+ private HistoryManager historyManager;
+ private ArrayAdapter adapter;
+ private CharSequence originalTitle;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ this.historyManager = new HistoryManager(this);
+ adapter = new HistoryItemAdapter(this);
+ setListAdapter(adapter);
+ View listview = getListView();
+ registerForContextMenu(listview);
+ originalTitle = getTitle();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ reloadHistoryItems();
+ }
+
+ private void reloadHistoryItems() {
+ Iterable items = historyManager.buildHistoryItems();
+ adapter.clear();
+ for (HistoryItem item : items) {
+ adapter.add(item);
+ }
+ setTitle(originalTitle + " (" + adapter.getCount() + ')');
+ if (adapter.isEmpty()) {
+ adapter.add(new HistoryItem(null, null, null));
+ }
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ if (adapter.getItem(position).getResult() != null) {
+ Intent intent = new Intent(this, CaptureActivity.class);
+ intent.putExtra(Intents.History.ITEM_NUMBER, position);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu,
+ View v,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ int position = ((AdapterView.AdapterContextMenuInfo) menuInfo).position;
+ if (position >= adapter.getCount() || adapter.getItem(position).getResult() != null) {
+ menu.add(Menu.NONE, position, position, R.string.history_clear_one_history_text);
+ } // else it's just that dummy "Empty" message
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ int position = item.getItemId();
+ historyManager.deleteHistoryItem(position);
+ reloadHistoryItems();
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (historyManager.hasHistoryItems()) {
+ MenuInflater menuInflater = getMenuInflater();
+ menuInflater.inflate(R.menu.history, menu);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_history_send:
+ CharSequence history = historyManager.buildHistory();
+ Parcelable historyFile = HistoryManager.saveHistory(history.toString());
+ if (historyFile == null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.msg_unmount_usb);
+ builder.setPositiveButton(R.string.button_ok, null);
+ builder.show();
+ } else {
+ Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:"));
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ String subject = getResources().getString(R.string.history_email_title);
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ intent.putExtra(Intent.EXTRA_TEXT, subject);
+ intent.putExtra(Intent.EXTRA_STREAM, historyFile);
+ intent.setType("text/csv");
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException anfe) {
+ Log.w(TAG, anfe.toString());
+ }
+ }
+ break;
+ case R.id.menu_history_clear_text:
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.msg_sure);
+ builder.setCancelable(true);
+ builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i2) {
+ historyManager.clearHistory();
+ dialog.dismiss();
+ finish();
+ }
+ });
+ builder.setNegativeButton(R.string.button_cancel, null);
+ builder.show();
+ break;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java
new file mode 100644
index 0000000..45da8bd
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryItem.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012 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.history;
+
+import com.google.zxing.Result;
+
+public final class HistoryItem {
+
+ private final Result result;
+ private final String display;
+ private final String details;
+
+ HistoryItem(Result result, String display, String details) {
+ this.result = result;
+ this.display = display;
+ this.details = details;
+ }
+
+ public Result getResult() {
+ return result;
+ }
+
+ public String getDisplayAndDetails() {
+ StringBuilder displayResult = new StringBuilder();
+ if (display == null || display.isEmpty()) {
+ displayResult.append(result.getText());
+ } else {
+ displayResult.append(display);
+ }
+ if (details != null && !details.isEmpty()) {
+ displayResult.append(" : ").append(details);
+ }
+ return displayResult.toString();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java
new file mode 100644
index 0000000..762ba2d
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryItemAdapter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2012 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.history;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.google.zxing.Result;
+import net.foucry.pilldroid.R;
+
+import java.util.ArrayList;
+
+final class HistoryItemAdapter extends ArrayAdapter {
+
+ private final Context activity;
+
+ HistoryItemAdapter(Context activity) {
+ super(activity, R.layout.history_list_item, new ArrayList());
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup viewGroup) {
+ View layout;
+ if (view instanceof LinearLayout) {
+ layout = view;
+ } else {
+ LayoutInflater factory = LayoutInflater.from(activity);
+ layout = factory.inflate(R.layout.history_list_item, viewGroup, false);
+ }
+
+ HistoryItem item = getItem(position);
+ Result result = item.getResult();
+
+ CharSequence title;
+ CharSequence detail;
+ if (result != null) {
+ title = result.getText();
+ detail = item.getDisplayAndDetails();
+ } else {
+ Resources resources = getContext().getResources();
+ title = resources.getString(R.string.history_empty);
+ detail = resources.getString(R.string.history_empty_detail);
+ }
+
+ ((TextView) layout.findViewById(R.id.history_title)).setText(title);
+ ((TextView) layout.findViewById(R.id.history_detail)).setText(detail);
+
+ return layout;
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java b/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java
new file mode 100644
index 0000000..dbb7934
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/history/HistoryManager.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2009 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.history;
+
+import android.database.sqlite.SQLiteException;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.Result;
+import com.google.zxing.client.android.Intents;
+import com.google.zxing.client.android.PreferencesActivity;
+import com.google.zxing.client.android.result.ResultHandler;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Manages functionality related to scan history.
+ *
+ * @author Sean Owen
+ */
+public final class HistoryManager {
+
+ private static final String TAG = HistoryManager.class.getSimpleName();
+
+ private static final int MAX_ITEMS = 2000;
+
+ private static final String[] COLUMNS = {
+ DBHelper.TEXT_COL,
+ DBHelper.DISPLAY_COL,
+ DBHelper.FORMAT_COL,
+ DBHelper.TIMESTAMP_COL,
+ DBHelper.DETAILS_COL,
+ };
+
+ private static final String[] COUNT_COLUMN = { "COUNT(1)" };
+
+ private static final String[] ID_COL_PROJECTION = { DBHelper.ID_COL };
+ private static final String[] ID_DETAIL_COL_PROJECTION = { DBHelper.ID_COL, DBHelper.DETAILS_COL };
+
+ private final Activity activity;
+ private final boolean enableHistory;
+
+ public HistoryManager(Activity activity) {
+ this.activity = activity;
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
+ enableHistory = prefs.getBoolean(PreferencesActivity.KEY_ENABLE_HISTORY, true);
+ }
+
+ public boolean hasHistoryItems() {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getReadableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME, COUNT_COLUMN, null, null, null, null, null);
+ cursor.moveToFirst();
+ return cursor.getInt(0) > 0;
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ public List buildHistoryItems() {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ List items = new ArrayList<>();
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getReadableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME, COLUMNS, null, null, null, null, DBHelper.TIMESTAMP_COL + " DESC");
+ while (cursor.moveToNext()) {
+ String text = cursor.getString(0);
+ String display = cursor.getString(1);
+ String format = cursor.getString(2);
+ long timestamp = cursor.getLong(3);
+ String details = cursor.getString(4);
+ Result result = new Result(text, null, null, BarcodeFormat.valueOf(format), timestamp);
+ items.add(new HistoryItem(result, display, details));
+ }
+ } finally {
+ close(cursor, db);
+ }
+ return items;
+ }
+
+ public HistoryItem buildHistoryItem(int number) {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getReadableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME, COLUMNS, null, null, null, null, DBHelper.TIMESTAMP_COL + " DESC");
+ cursor.move(number + 1);
+ String text = cursor.getString(0);
+ String display = cursor.getString(1);
+ String format = cursor.getString(2);
+ long timestamp = cursor.getLong(3);
+ String details = cursor.getString(4);
+ Result result = new Result(text, null, null, BarcodeFormat.valueOf(format), timestamp);
+ return new HistoryItem(result, display, details);
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ public void deleteHistoryItem(int number) {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getWritableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME,
+ ID_COL_PROJECTION,
+ null, null, null, null,
+ DBHelper.TIMESTAMP_COL + " DESC");
+ cursor.move(number + 1);
+ db.delete(DBHelper.TABLE_NAME, DBHelper.ID_COL + '=' + cursor.getString(0), null);
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ public void addHistoryItem(Result result, ResultHandler handler) {
+ // Do not save this item to the history if the preference is turned off, or the contents are
+ // considered secure.
+ if (!activity.getIntent().getBooleanExtra(Intents.Scan.SAVE_HISTORY, true) ||
+ handler.areContentsSecure() || !enableHistory) {
+ return;
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
+ if (!prefs.getBoolean(PreferencesActivity.KEY_REMEMBER_DUPLICATES, false)) {
+ deletePrevious(result.getText());
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(DBHelper.TEXT_COL, result.getText());
+ values.put(DBHelper.FORMAT_COL, result.getBarcodeFormat().toString());
+ values.put(DBHelper.DISPLAY_COL, handler.getDisplayContents().toString());
+ values.put(DBHelper.TIMESTAMP_COL, System.currentTimeMillis());
+
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ try {
+ db = helper.getWritableDatabase();
+ // Insert the new entry into the DB.
+ db.insert(DBHelper.TABLE_NAME, DBHelper.TIMESTAMP_COL, values);
+ } finally {
+ close(null, db);
+ }
+ }
+
+ public void addHistoryItemDetails(String itemID, String itemDetails) {
+ // As we're going to do an update only we don't need need to worry
+ // about the preferences; if the item wasn't saved it won't be udpated
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getWritableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME,
+ ID_DETAIL_COL_PROJECTION,
+ DBHelper.TEXT_COL + "=?",
+ new String[] { itemID },
+ null,
+ null,
+ DBHelper.TIMESTAMP_COL + " DESC",
+ "1");
+ String oldID = null;
+ String oldDetails = null;
+ if (cursor.moveToNext()) {
+ oldID = cursor.getString(0);
+ oldDetails = cursor.getString(1);
+ }
+
+ if (oldID != null) {
+ String newDetails;
+ if (oldDetails == null) {
+ newDetails = itemDetails;
+ } else if (oldDetails.contains(itemDetails)) {
+ newDetails = null;
+ } else {
+ newDetails = oldDetails + " : " + itemDetails;
+ }
+ if (newDetails != null) {
+ ContentValues values = new ContentValues();
+ values.put(DBHelper.DETAILS_COL, newDetails);
+ db.update(DBHelper.TABLE_NAME, values, DBHelper.ID_COL + "=?", new String[] { oldID });
+ }
+ }
+
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ private void deletePrevious(String text) {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ try {
+ db = helper.getWritableDatabase();
+ db.delete(DBHelper.TABLE_NAME, DBHelper.TEXT_COL + "=?", new String[] { text });
+ } finally {
+ close(null, db);
+ }
+ }
+
+ public void trimHistory() {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getWritableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME,
+ ID_COL_PROJECTION,
+ null, null, null, null,
+ DBHelper.TIMESTAMP_COL + " DESC");
+ cursor.move(MAX_ITEMS);
+ while (cursor.moveToNext()) {
+ String id = cursor.getString(0);
+ Log.i(TAG, "Deleting scan history ID " + id);
+ db.delete(DBHelper.TABLE_NAME, DBHelper.ID_COL + '=' + id, null);
+ }
+ } catch (SQLiteException sqle) {
+ // We're seeing an error here when called in CaptureActivity.onCreate() in rare cases
+ // and don't understand it. First theory is that it's transient so can be safely ignored.
+ Log.w(TAG, sqle);
+ // continue
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ /**
+ * Builds a text representation of the scanning history. Each scan is encoded on one
+ * line, terminated by a line break (\r\n). The values in each line are comma-separated,
+ * and double-quoted. Double-quotes within values are escaped with a sequence of two
+ * double-quotes. The fields output are:
+ *
+ *
+ * Raw text
+ * Display text
+ * Format (e.g. QR_CODE)
+ * Unix timestamp (milliseconds since the epoch)
+ * Formatted version of timestamp
+ * Supplemental info (e.g. price info for a product barcode)
+ *
+ */
+ CharSequence buildHistory() {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ Cursor cursor = null;
+ try {
+ db = helper.getWritableDatabase();
+ cursor = db.query(DBHelper.TABLE_NAME,
+ COLUMNS,
+ null, null, null, null,
+ DBHelper.TIMESTAMP_COL + " DESC");
+
+ DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
+ StringBuilder historyText = new StringBuilder(1000);
+ while (cursor.moveToNext()) {
+
+ historyText.append('"').append(massageHistoryField(cursor.getString(0))).append("\",");
+ historyText.append('"').append(massageHistoryField(cursor.getString(1))).append("\",");
+ historyText.append('"').append(massageHistoryField(cursor.getString(2))).append("\",");
+ historyText.append('"').append(massageHistoryField(cursor.getString(3))).append("\",");
+
+ // Add timestamp again, formatted
+ long timestamp = cursor.getLong(3);
+ historyText.append('"').append(massageHistoryField(
+ format.format(new Date(timestamp)))).append("\",");
+
+ // Above we're preserving the old ordering of columns which had formatted data in position 5
+
+ historyText.append('"').append(massageHistoryField(cursor.getString(4))).append("\"\r\n");
+ }
+ return historyText;
+ } finally {
+ close(cursor, db);
+ }
+ }
+
+ void clearHistory() {
+ SQLiteOpenHelper helper = new DBHelper(activity);
+ SQLiteDatabase db = null;
+ try {
+ db = helper.getWritableDatabase();
+ db.delete(DBHelper.TABLE_NAME, null, null);
+ } finally {
+ close(null, db);
+ }
+ }
+
+ static Uri saveHistory(String history) {
+ File bsRoot = new File(Environment.getExternalStorageDirectory(), "BarcodeScanner");
+ File historyRoot = new File(bsRoot, "History");
+ if (!historyRoot.exists() && !historyRoot.mkdirs()) {
+ Log.w(TAG, "Couldn't make dir " + historyRoot);
+ return null;
+ }
+ File historyFile = new File(historyRoot, "history-" + System.currentTimeMillis() + ".csv");
+ OutputStreamWriter out = null;
+ try {
+ out = new OutputStreamWriter(new FileOutputStream(historyFile), Charset.forName("UTF-8"));
+ out.write(history);
+ return Uri.parse("file://" + historyFile.getAbsolutePath());
+ } catch (IOException ioe) {
+ Log.w(TAG, "Couldn't access file " + historyFile + " due to " + ioe);
+ return null;
+ } finally {
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException ioe) {
+ // do nothing
+ }
+ }
+ }
+ }
+
+ private static String massageHistoryField(String value) {
+ return value == null ? "" : value.replace("\"","\"\"");
+ }
+
+ private static void close(Cursor cursor, SQLiteDatabase database) {
+ if (cursor != null) {
+ cursor.close();
+ }
+ if (database != null) {
+ database.close();
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java
new file mode 100644
index 0000000..4c69e3a
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/AddressBookResultHandler.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+import android.graphics.Typeface;
+import android.telephony.PhoneNumberUtils;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+
+import com.google.zxing.client.result.AddressBookParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Handles address book entries.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class AddressBookResultHandler extends ResultHandler {
+
+ private static final DateFormat[] DATE_FORMATS = {
+ new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH),
+ new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH),
+ };
+ static {
+ for (DateFormat format : DATE_FORMATS) {
+ format.setLenient(false);
+ }
+ }
+
+ private static final int[] BUTTON_TEXTS = {
+ R.string.button_add_contact,
+ R.string.button_show_map,
+ R.string.button_dial,
+ R.string.button_email,
+ };
+
+ private final boolean[] fields;
+ private int buttonCount;
+
+ // This takes all the work out of figuring out which buttons/actions should be in which
+ // positions, based on which fields are present in this barcode.
+ private int mapIndexToAction(int index) {
+ if (index < buttonCount) {
+ int count = -1;
+ for (int x = 0; x < MAX_BUTTON_COUNT; x++) {
+ if (fields[x]) {
+ count++;
+ }
+ if (count == index) {
+ return x;
+ }
+ }
+ }
+ return -1;
+ }
+
+ public AddressBookResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ AddressBookParsedResult addressResult = (AddressBookParsedResult) result;
+ String[] addresses = addressResult.getAddresses();
+ boolean hasAddress = addresses != null && addresses.length > 0 && addresses[0] != null && !addresses[0].isEmpty();
+ String[] phoneNumbers = addressResult.getPhoneNumbers();
+ boolean hasPhoneNumber = phoneNumbers != null && phoneNumbers.length > 0;
+ String[] emails = addressResult.getEmails();
+ boolean hasEmailAddress = emails != null && emails.length > 0;
+
+ fields = new boolean[MAX_BUTTON_COUNT];
+ fields[0] = true; // Add contact is always available
+ fields[1] = hasAddress;
+ fields[2] = hasPhoneNumber;
+ fields[3] = hasEmailAddress;
+
+ buttonCount = 0;
+ for (int x = 0; x < MAX_BUTTON_COUNT; x++) {
+ if (fields[x]) {
+ buttonCount++;
+ }
+ }
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttonCount;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return BUTTON_TEXTS[mapIndexToAction(index)];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ AddressBookParsedResult addressResult = (AddressBookParsedResult) getResult();
+ String[] addresses = addressResult.getAddresses();
+ String address1 = addresses == null || addresses.length < 1 ? null : addresses[0];
+ String[] addressTypes = addressResult.getAddressTypes();
+ String address1Type = addressTypes == null || addressTypes.length < 1 ? null : addressTypes[0];
+ int action = mapIndexToAction(index);
+ switch (action) {
+ case 0:
+ addContact(addressResult.getNames(),
+ addressResult.getNicknames(),
+ addressResult.getPronunciation(),
+ addressResult.getPhoneNumbers(),
+ addressResult.getPhoneTypes(),
+ addressResult.getEmails(),
+ addressResult.getEmailTypes(),
+ addressResult.getNote(),
+ addressResult.getInstantMessenger(),
+ address1,
+ address1Type,
+ addressResult.getOrg(),
+ addressResult.getTitle(),
+ addressResult.getURLs(),
+ addressResult.getBirthday(),
+ addressResult.getGeo());
+ break;
+ case 1:
+ searchMap(address1);
+ break;
+ case 2:
+ dialPhone(addressResult.getPhoneNumbers()[0]);
+ break;
+ case 3:
+ sendEmail(addressResult.getEmails(), null, null, null, null);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static Date parseDate(String s) {
+ for (DateFormat currentFormat : DATE_FORMATS) {
+ try {
+ return currentFormat.parse(s);
+ } catch (ParseException e) {
+ // continue
+ }
+ }
+ return null;
+ }
+
+ // Overriden so we can hyphenate phone numbers, format birthdays, and bold the name.
+ @Override
+ public CharSequence getDisplayContents() {
+ AddressBookParsedResult result = (AddressBookParsedResult) getResult();
+ StringBuilder contents = new StringBuilder(100);
+ ParsedResult.maybeAppend(result.getNames(), contents);
+ int namesLength = contents.length();
+
+ String pronunciation = result.getPronunciation();
+ if (pronunciation != null && !pronunciation.isEmpty()) {
+ contents.append("\n(");
+ contents.append(pronunciation);
+ contents.append(')');
+ }
+
+ ParsedResult.maybeAppend(result.getTitle(), contents);
+ ParsedResult.maybeAppend(result.getOrg(), contents);
+ ParsedResult.maybeAppend(result.getAddresses(), contents);
+ String[] numbers = result.getPhoneNumbers();
+ if (numbers != null) {
+ for (String number : numbers) {
+ if (number != null) {
+ ParsedResult.maybeAppend(PhoneNumberUtils.formatNumber(number), contents);
+ }
+ }
+ }
+ ParsedResult.maybeAppend(result.getEmails(), contents);
+ ParsedResult.maybeAppend(result.getURLs(), contents);
+
+ String birthday = result.getBirthday();
+ if (birthday != null && !birthday.isEmpty()) {
+ Date date = parseDate(birthday);
+ if (date != null) {
+ ParsedResult.maybeAppend(DateFormat.getDateInstance(DateFormat.MEDIUM).format(date.getTime()), contents);
+ }
+ }
+ ParsedResult.maybeAppend(result.getNote(), contents);
+
+ if (namesLength > 0) {
+ // Bold the full name to make it stand out a bit.
+ Spannable styled = new SpannableString(contents.toString());
+ styled.setSpan(new StyleSpan(Typeface.BOLD), 0, namesLength, 0);
+ return styled;
+ } else {
+ return contents.toString();
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_address_book;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java
new file mode 100644
index 0000000..31438df
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/CalendarResultHandler.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.util.Log;
+
+import com.google.zxing.client.result.CalendarParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Handles calendar entries encoded in QR Codes.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ * @author Sean Owen
+ */
+public final class CalendarResultHandler extends ResultHandler {
+
+ private static final String TAG = CalendarResultHandler.class.getSimpleName();
+
+ private static final int[] buttons = {
+ R.string.button_add_calendar
+ };
+
+ public CalendarResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttons.length;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ if (index == 0) {
+ CalendarParsedResult calendarResult = (CalendarParsedResult) getResult();
+
+ String description = calendarResult.getDescription();
+ String organizer = calendarResult.getOrganizer();
+ if (organizer != null) { // No separate Intent key, put in description
+ if (description == null) {
+ description = organizer;
+ } else {
+ description = description + '\n' + organizer;
+ }
+ }
+
+ addCalendarEvent(calendarResult.getSummary(),
+ calendarResult.getStart(),
+ calendarResult.isStartAllDay(),
+ calendarResult.getEnd(),
+ calendarResult.getLocation(),
+ description,
+ calendarResult.getAttendees());
+ }
+ }
+
+ /**
+ * Sends an intent to create a new calendar event by prepopulating the Add Event UI. Older
+ * versions of the system have a bug where the event title will not be filled out.
+ *
+ * @param summary A description of the event
+ * @param start The start time
+ * @param allDay if true, event is considered to be all day starting from start time
+ * @param end The end time (optional)
+ * @param location a text description of the event location
+ * @param description a text description of the event itself
+ * @param attendees attendees to invite
+ */
+ private void addCalendarEvent(String summary,
+ Date start,
+ boolean allDay,
+ Date end,
+ String location,
+ String description,
+ String[] attendees) {
+ Intent intent = new Intent(Intent.ACTION_INSERT);
+ intent.setType("vnd.android.cursor.item/event");
+ long startMilliseconds = start.getTime();
+ intent.putExtra("beginTime", startMilliseconds);
+ if (allDay) {
+ intent.putExtra("allDay", true);
+ }
+ long endMilliseconds;
+ if (end == null) {
+ if (allDay) {
+ // + 1 day
+ endMilliseconds = startMilliseconds + 24 * 60 * 60 * 1000;
+ } else {
+ endMilliseconds = startMilliseconds;
+ }
+ } else {
+ endMilliseconds = end.getTime();
+ }
+ intent.putExtra("endTime", endMilliseconds);
+ intent.putExtra("title", summary);
+ intent.putExtra("eventLocation", location);
+ intent.putExtra("description", description);
+ if (attendees != null) {
+ intent.putExtra(Intent.EXTRA_EMAIL, attendees);
+ // Documentation says this is either a String[] or comma-separated String, which is right?
+ }
+
+ try {
+ // Do this manually at first
+ rawLaunchIntent(intent);
+ } catch (ActivityNotFoundException anfe) {
+ Log.w(TAG, "No calendar app available that responds to " + Intent.ACTION_INSERT);
+ // For calendar apps that don't like "INSERT":
+ intent.setAction(Intent.ACTION_EDIT);
+ launchIntent(intent); // Fail here for real if nothing can handle it
+ }
+ }
+
+
+ @Override
+ public CharSequence getDisplayContents() {
+
+ CalendarParsedResult calResult = (CalendarParsedResult) getResult();
+ StringBuilder result = new StringBuilder(100);
+
+ ParsedResult.maybeAppend(calResult.getSummary(), result);
+
+ Date start = calResult.getStart();
+ ParsedResult.maybeAppend(format(calResult.isStartAllDay(), start), result);
+
+ Date end = calResult.getEnd();
+ if (end != null) {
+ if (calResult.isEndAllDay() && !start.equals(end)) {
+ // Show only year/month/day
+ // if it's all-day and this is the end date, it's exclusive, so show the user
+ // that it ends on the day before to make more intuitive sense.
+ // But don't do it if the event already (incorrectly?) specifies the same start/end
+ end = new Date(end.getTime() - 24 * 60 * 60 * 1000);
+ }
+ ParsedResult.maybeAppend(format(calResult.isEndAllDay(), end), result);
+ }
+
+ ParsedResult.maybeAppend(calResult.getLocation(), result);
+ ParsedResult.maybeAppend(calResult.getOrganizer(), result);
+ ParsedResult.maybeAppend(calResult.getAttendees(), result);
+ ParsedResult.maybeAppend(calResult.getDescription(), result);
+ return result.toString();
+ }
+
+ private static String format(boolean allDay, Date date) {
+ if (date == null) {
+ return null;
+ }
+ DateFormat format = allDay
+ ? DateFormat.getDateInstance(DateFormat.MEDIUM)
+ : DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
+ return format.format(date);
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_calendar;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java
new file mode 100644
index 0000000..61631a3
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/EmailAddressResultHandler.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.client.result.EmailAddressParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles email addresses.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class EmailAddressResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_email,
+ R.string.button_add_contact
+ };
+
+ public EmailAddressResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttons.length;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ EmailAddressParsedResult emailResult = (EmailAddressParsedResult) getResult();
+ switch (index) {
+ case 0:
+ sendEmail(emailResult.getTos(),
+ emailResult.getCCs(),
+ emailResult.getBCCs(),
+ emailResult.getSubject(),
+ emailResult.getBody());
+ break;
+ case 1:
+ addEmailOnlyContact(emailResult.getTos(), null);
+ break;
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_email_address;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java
new file mode 100644
index 0000000..6d3d855
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/GeoResultHandler.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.client.result.GeoParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles geographic coordinates (typically encoded as geo: URLs).
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class GeoResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_show_map,
+ R.string.button_get_directions
+ };
+
+ public GeoResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttons.length;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ GeoParsedResult geoResult = (GeoParsedResult) getResult();
+ switch (index) {
+ case 0:
+ openMap(geoResult.getGeoURI());
+ break;
+ case 1:
+ getDirections(geoResult.getLatitude(), geoResult.getLongitude());
+ break;
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_geo;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java
new file mode 100644
index 0000000..085cadb
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/ISBNResultHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.Result;
+import com.google.zxing.client.result.ISBNParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles books encoded by their ISBN values.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ISBNResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_product_search,
+ R.string.button_book_search,
+ R.string.button_search_book_contents,
+ R.string.button_custom_product_search
+ };
+
+ public ISBNResultHandler(Activity activity, ParsedResult result, Result rawResult) {
+ super(activity, result, rawResult);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return hasCustomProductSearch() ? buttons.length : buttons.length - 1;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ ISBNParsedResult isbnResult = (ISBNParsedResult) getResult();
+ switch (index) {
+ case 0:
+ openProductSearch(isbnResult.getISBN());
+ break;
+ case 1:
+ openBookSearch(isbnResult.getISBN());
+ break;
+ case 2:
+ searchBookContents(isbnResult.getISBN());
+ break;
+ case 3:
+ openURL(fillInCustomSearchURL(isbnResult.getISBN()));
+ break;
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_isbn;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java
new file mode 100644
index 0000000..11772ab
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/ProductResultHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.Result;
+import com.google.zxing.client.result.ExpandedProductParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.ProductParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles generic products which are not books.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ProductResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_product_search,
+ R.string.button_web_search,
+ R.string.button_custom_product_search
+ };
+
+ public ProductResultHandler(Activity activity, ParsedResult result, Result rawResult) {
+ super(activity, result, rawResult);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return hasCustomProductSearch() ? buttons.length : buttons.length - 1;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ String productID = getProductIDFromResult(getResult());
+ switch (index) {
+ case 0:
+ openProductSearch(productID);
+ break;
+ case 1:
+ webSearch(productID);
+ break;
+ case 2:
+ openURL(fillInCustomSearchURL(productID));
+ break;
+ }
+ }
+
+ private static String getProductIDFromResult(ParsedResult rawResult) {
+ if (rawResult instanceof ProductParsedResult) {
+ return ((ProductParsedResult) rawResult).getNormalizedProductID();
+ }
+ if (rawResult instanceof ExpandedProductParsedResult) {
+ return ((ExpandedProductParsedResult) rawResult).getRawText();
+ }
+ throw new IllegalArgumentException(rawResult.getClass().toString());
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_product;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java b/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java
new file mode 100644
index 0000000..2e107c4
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/ResultButtonListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.view.View;
+
+/**
+ * Handles the result of barcode decoding in the context of the Android platform, by dispatching the
+ * proper intents to open other activities like GMail, Maps, etc.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ResultButtonListener implements View.OnClickListener {
+ private final ResultHandler resultHandler;
+ private final int index;
+
+ public ResultButtonListener(ResultHandler resultHandler, int index) {
+ this.resultHandler = resultHandler;
+ this.index = index;
+ }
+
+ @Override
+ public void onClick(View view) {
+ resultHandler.handleButtonPress(index);
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java
new file mode 100644
index 0000000..086e102
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/ResultHandler.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.google.zxing.Result;
+import com.google.zxing.client.android.Contents;
+import com.google.zxing.client.android.Intents;
+import com.google.zxing.client.android.LocaleManager;
+import com.google.zxing.client.android.PreferencesActivity;
+import com.google.zxing.client.android.book.SearchBookContentsActivity;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.ParsedResultType;
+import com.google.zxing.client.result.ResultParser;
+
+import net.foucry.pilldroid.R;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Locale;
+
+//import com.google.zxing.client.android.R;
+
+/**
+ * A base class for the Android-specific barcode handlers. These allow the app to polymorphically
+ * suggest the appropriate actions for each data type.
+ *
+ * This class also contains a bunch of utility methods to take common actions like opening a URL.
+ * They could easily be moved into a helper object, but it can't be static because the Activity
+ * instance is needed to launch an intent.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ * @author Sean Owen
+ */
+public abstract class ResultHandler {
+
+ private static final String TAG = ResultHandler.class.getSimpleName();
+
+ private static final String[] EMAIL_TYPE_STRINGS = {"home", "work", "mobile"};
+ private static final String[] PHONE_TYPE_STRINGS = {"home", "work", "mobile", "fax", "pager", "main"};
+ private static final String[] ADDRESS_TYPE_STRINGS = {"home", "work"};
+ private static final int[] EMAIL_TYPE_VALUES = {
+ ContactsContract.CommonDataKinds.Email.TYPE_HOME,
+ ContactsContract.CommonDataKinds.Email.TYPE_WORK,
+ ContactsContract.CommonDataKinds.Email.TYPE_MOBILE,
+ };
+ private static final int[] PHONE_TYPE_VALUES = {
+ ContactsContract.CommonDataKinds.Phone.TYPE_HOME,
+ ContactsContract.CommonDataKinds.Phone.TYPE_WORK,
+ ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
+ ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK,
+ ContactsContract.CommonDataKinds.Phone.TYPE_PAGER,
+ ContactsContract.CommonDataKinds.Phone.TYPE_MAIN,
+ };
+ private static final int[] ADDRESS_TYPE_VALUES = {
+ ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME,
+ ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK,
+ };
+ private static final int NO_TYPE = -1;
+
+ public static final int MAX_BUTTON_COUNT = 4;
+
+ private final ParsedResult result;
+ private final Activity activity;
+ private final Result rawResult;
+ private final String customProductSearch;
+
+ ResultHandler(Activity activity, ParsedResult result) {
+ this(activity, result, null);
+ }
+
+ ResultHandler(Activity activity, ParsedResult result, Result rawResult) {
+ this.result = result;
+ this.activity = activity;
+ this.rawResult = rawResult;
+ this.customProductSearch = parseCustomSearchURL();
+ }
+
+ public final ParsedResult getResult() {
+ return result;
+ }
+
+ final boolean hasCustomProductSearch() {
+ return customProductSearch != null;
+ }
+
+ final Activity getActivity() {
+ return activity;
+ }
+
+ /**
+ * Indicates how many buttons the derived class wants shown.
+ *
+ * @return The integer button count.
+ */
+ public abstract int getButtonCount();
+
+ /**
+ * The text of the nth action button.
+ *
+ * @param index From 0 to getButtonCount() - 1
+ * @return The button text as a resource ID
+ */
+ public abstract int getButtonText(int index);
+
+ public Integer getDefaultButtonID() {
+ return null;
+ }
+
+ /**
+ * Execute the action which corresponds to the nth button.
+ *
+ * @param index The button that was clicked.
+ */
+ 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;
+ }
+
+ /**
+ * Create a possibly styled string for the contents of the current barcode.
+ *
+ * @return The text to be displayed.
+ */
+ public CharSequence getDisplayContents() {
+ String contents = result.getDisplayResult();
+ return contents.replace("\r", "");
+ }
+
+ /**
+ * A string describing the kind of barcode that was found, e.g. "Found contact info".
+ *
+ * @return The resource ID of the string.
+ */
+ public abstract int getDisplayTitle();
+
+ /**
+ * A convenience method to get the parsed type. Should not be overridden.
+ *
+ * @return The parsed type, e.g. URI or ISBN
+ */
+ public final ParsedResultType getType() {
+ return result.getType();
+ }
+
+ final void addPhoneOnlyContact(String[] phoneNumbers,String[] phoneTypes) {
+ addContact(null, null, null, phoneNumbers, phoneTypes, null, null, null, null, null, null, null, null, null, null, null);
+ }
+
+ final void addEmailOnlyContact(String[] emails, String[] emailTypes) {
+ addContact(null, null, null, null, null, emails, emailTypes, null, null, null, null, null, null, null, null, null);
+ }
+
+ final void addContact(String[] names,
+ String[] nicknames,
+ String pronunciation,
+ String[] phoneNumbers,
+ String[] phoneTypes,
+ String[] emails,
+ String[] emailTypes,
+ String note,
+ String instantMessenger,
+ String address,
+ String addressType,
+ String org,
+ String title,
+ String[] urls,
+ String birthday,
+ String[] geo) {
+
+ // Only use the first name in the array, if present.
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT, ContactsContract.Contacts.CONTENT_URI);
+ intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+ putExtra(intent, ContactsContract.Intents.Insert.NAME, names != null ? names[0] : null);
+
+ putExtra(intent, ContactsContract.Intents.Insert.PHONETIC_NAME, pronunciation);
+
+ int phoneCount = Math.min(phoneNumbers != null ? phoneNumbers.length : 0, Contents.PHONE_KEYS.length);
+ for (int x = 0; x < phoneCount; x++) {
+ putExtra(intent, Contents.PHONE_KEYS[x], phoneNumbers[x]);
+ if (phoneTypes != null && x < phoneTypes.length) {
+ int type = toPhoneContractType(phoneTypes[x]);
+ if (type >= 0) {
+ intent.putExtra(Contents.PHONE_TYPE_KEYS[x], type);
+ }
+ }
+ }
+
+ int emailCount = Math.min(emails != null ? emails.length : 0, Contents.EMAIL_KEYS.length);
+ for (int x = 0; x < emailCount; x++) {
+ putExtra(intent, Contents.EMAIL_KEYS[x], emails[x]);
+ if (emailTypes != null && x < emailTypes.length) {
+ int type = toEmailContractType(emailTypes[x]);
+ if (type >= 0) {
+ intent.putExtra(Contents.EMAIL_TYPE_KEYS[x], type);
+ }
+ }
+ }
+
+ ArrayList data = new ArrayList<>();
+ if (urls != null) {
+ for (String url : urls) {
+ if (url != null && !url.isEmpty()) {
+ ContentValues row = new ContentValues(2);
+ row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
+ row.put(ContactsContract.CommonDataKinds.Website.URL, url);
+ data.add(row);
+ break;
+ }
+ }
+ }
+
+ if (birthday != null) {
+ ContentValues row = new ContentValues(3);
+ row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
+ row.put(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY);
+ row.put(ContactsContract.CommonDataKinds.Event.START_DATE, birthday);
+ data.add(row);
+ }
+
+ if (nicknames != null) {
+ for (String nickname : nicknames) {
+ if (nickname != null && !nickname.isEmpty()) {
+ ContentValues row = new ContentValues(3);
+ row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
+ row.put(ContactsContract.CommonDataKinds.Nickname.TYPE,
+ ContactsContract.CommonDataKinds.Nickname.TYPE_DEFAULT);
+ row.put(ContactsContract.CommonDataKinds.Nickname.NAME, nickname);
+ data.add(row);
+ break;
+ }
+ }
+ }
+
+ if (!data.isEmpty()) {
+ intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, data);
+ }
+
+ StringBuilder aggregatedNotes = new StringBuilder();
+ if (note != null) {
+ aggregatedNotes.append('\n').append(note);
+ }
+ if (geo != null) {
+ aggregatedNotes.append('\n').append(geo[0]).append(',').append(geo[1]);
+ }
+
+ if (aggregatedNotes.length() > 0) {
+ // Remove extra leading '\n'
+ putExtra(intent, ContactsContract.Intents.Insert.NOTES, aggregatedNotes.substring(1));
+ }
+
+ putExtra(intent, ContactsContract.Intents.Insert.IM_HANDLE, instantMessenger);
+ putExtra(intent, ContactsContract.Intents.Insert.POSTAL, address);
+ if (addressType != null) {
+ int type = toAddressContractType(addressType);
+ if (type >= 0) {
+ intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, type);
+ }
+ }
+ putExtra(intent, ContactsContract.Intents.Insert.COMPANY, org);
+ putExtra(intent, ContactsContract.Intents.Insert.JOB_TITLE, title);
+ launchIntent(intent);
+ }
+
+ private static int toEmailContractType(String typeString) {
+ return doToContractType(typeString, EMAIL_TYPE_STRINGS, EMAIL_TYPE_VALUES);
+ }
+
+ private static int toPhoneContractType(String typeString) {
+ return doToContractType(typeString, PHONE_TYPE_STRINGS, PHONE_TYPE_VALUES);
+ }
+
+ private static int toAddressContractType(String typeString) {
+ return doToContractType(typeString, ADDRESS_TYPE_STRINGS, ADDRESS_TYPE_VALUES);
+ }
+
+ private static int doToContractType(String typeString, String[] types, int[] values) {
+ if (typeString == null) {
+ return NO_TYPE;
+ }
+ for (int i = 0; i < types.length; i++) {
+ String type = types[i];
+ if (typeString.startsWith(type) || typeString.startsWith(type.toUpperCase(Locale.ENGLISH))) {
+ return values[i];
+ }
+ }
+ return NO_TYPE;
+ }
+
+ final void shareByEmail(String contents) {
+ sendEmail(null, null, null, null, contents);
+ }
+
+ final void sendEmail(String[] to,
+ String[] cc,
+ String[] bcc,
+ String subject,
+ String body) {
+ Intent intent = new Intent(Intent.ACTION_SEND, Uri.parse("mailto:"));
+ if (to != null && to.length != 0) {
+ intent.putExtra(Intent.EXTRA_EMAIL, to);
+ }
+ if (cc != null && cc.length != 0) {
+ intent.putExtra(Intent.EXTRA_CC, cc);
+ }
+ if (bcc != null && bcc.length != 0) {
+ intent.putExtra(Intent.EXTRA_BCC, bcc);
+ }
+ putExtra(intent, Intent.EXTRA_SUBJECT, subject);
+ putExtra(intent, Intent.EXTRA_TEXT, body);
+ intent.setType("text/plain");
+ launchIntent(intent);
+ }
+
+ final void shareBySMS(String contents) {
+ sendSMSFromUri("smsto:", contents);
+ }
+
+ final void sendSMS(String phoneNumber, String body) {
+ sendSMSFromUri("smsto:" + phoneNumber, body);
+ }
+
+ final void sendSMSFromUri(String uri, String body) {
+ Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri));
+ putExtra(intent, "sms_body", body);
+ // Exit the app once the SMS is sent
+ intent.putExtra("compose_mode", true);
+ launchIntent(intent);
+ }
+
+ final void sendMMS(String phoneNumber, String subject, String body) {
+ sendMMSFromUri("mmsto:" + phoneNumber, subject, body);
+ }
+
+ final void sendMMSFromUri(String uri, String subject, String body) {
+ Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri));
+ // The Messaging app needs to see a valid subject or else it will treat this an an SMS.
+ if (subject == null || subject.isEmpty()) {
+ putExtra(intent, "subject", activity.getString(R.string.msg_default_mms_subject));
+ } else {
+ putExtra(intent, "subject", subject);
+ }
+ putExtra(intent, "sms_body", body);
+ intent.putExtra("compose_mode", true);
+ launchIntent(intent);
+ }
+
+ final void dialPhone(String phoneNumber) {
+ launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber)));
+ }
+
+ final void dialPhoneFromUri(String uri) {
+ launchIntent(new Intent(Intent.ACTION_DIAL, Uri.parse(uri)));
+ }
+
+ final void openMap(String geoURI) {
+ launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(geoURI)));
+ }
+
+ /**
+ * Do a geo search using the address as the query.
+ *
+ * @param address The address to find
+ */
+ final void searchMap(String address) {
+ launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=" + Uri.encode(address))));
+ }
+
+ final void getDirections(double latitude, double longitude) {
+ launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("http://maps.google." +
+ LocaleManager.getCountryTLD(activity) + "/maps?f=d&daddr=" + latitude + ',' + longitude)));
+ }
+
+ // Uses the mobile-specific version of Product Search, which is formatted for small screens.
+ final void openProductSearch(String upc) {
+ Uri uri = Uri.parse("http://www.google." + LocaleManager.getProductSearchCountryTLD(activity) +
+ "/m/products?q=" + upc + "&source=zxing");
+ launchIntent(new Intent(Intent.ACTION_VIEW, uri));
+ }
+
+ final void openBookSearch(String isbn) {
+ Uri uri = Uri.parse("http://books.google." + LocaleManager.getBookSearchCountryTLD(activity) +
+ "/books?vid=isbn" + isbn);
+ launchIntent(new Intent(Intent.ACTION_VIEW, uri));
+ }
+
+ final void searchBookContents(String isbnOrUrl) {
+ Intent intent = new Intent(Intents.SearchBookContents.ACTION);
+ intent.setClassName(activity, SearchBookContentsActivity.class.getName());
+ putExtra(intent, Intents.SearchBookContents.ISBN, isbnOrUrl);
+ launchIntent(intent);
+ }
+
+ final void openURL(String url) {
+ // Strangely, some Android browsers don't seem to register to handle HTTP:// or HTTPS://.
+ // Lower-case these as it should always be OK to lower-case these schemes.
+ if (url.startsWith("HTTP://")) {
+ url = "http" + url.substring(4);
+ } else if (url.startsWith("HTTPS://")) {
+ url = "https" + url.substring(5);
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ try {
+ launchIntent(intent);
+ } catch (ActivityNotFoundException ignored) {
+ Log.w(TAG, "Nothing available to handle " + intent);
+ }
+ }
+
+ final void webSearch(String query) {
+ Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+ intent.putExtra("query", query);
+ launchIntent(intent);
+ }
+
+ /**
+ * Like {@link #launchIntent(Intent)} but will tell you if it is not handle-able
+ * via {@link ActivityNotFoundException}.
+ *
+ * @throws ActivityNotFoundException
+ */
+ final void rawLaunchIntent(Intent intent) {
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ Log.d(TAG, "Launching intent: " + intent + " with extras: " + intent.getExtras());
+ activity.startActivity(intent);
+ }
+ }
+
+ /**
+ * Like {@link #rawLaunchIntent(Intent)} but will show a user dialog if nothing is available to handle.
+ */
+ final void launchIntent(Intent intent) {
+ try {
+ rawLaunchIntent(intent);
+ } catch (ActivityNotFoundException ignored) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.app_name);
+ builder.setMessage(R.string.msg_intent_failed);
+ builder.setPositiveButton(R.string.button_ok, null);
+ builder.show();
+ }
+ }
+
+ private static void putExtra(Intent intent, String key, String value) {
+ if (value != null && !value.isEmpty()) {
+ intent.putExtra(key, value);
+ }
+ }
+
+ private String parseCustomSearchURL() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
+ String customProductSearch = prefs.getString(PreferencesActivity.KEY_CUSTOM_PRODUCT_SEARCH,
+ null);
+ if (customProductSearch != null && customProductSearch.trim().isEmpty()) {
+ return null;
+ }
+ return customProductSearch;
+ }
+
+ final String fillInCustomSearchURL(String text) {
+ if (customProductSearch == null) {
+ return text; // ?
+ }
+ try {
+ text = URLEncoder.encode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // can't happen; UTF-8 is always supported. Continue, I guess, without encoding
+ }
+ String url = customProductSearch;
+ if (rawResult != null) {
+ // Replace %f but only if it doesn't seem to be a hex escape sequence. This remains
+ // problematic but avoids the more surprising problem of breaking escapes
+ url = url.replaceFirst("%f(?![0-9a-f])", rawResult.getBarcodeFormat().toString());
+ if (url.contains("%t")) {
+ ParsedResult parsedResultAgain = ResultParser.parseResult(rawResult);
+ url = url.replace("%t", parsedResultAgain.getType().toString());
+ }
+ }
+ // Replace %s last as it might contain itself %f or %t
+ return url.replace("%s", text);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java b/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java
new file mode 100644
index 0000000..a31d089
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/ResultHandlerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import com.google.zxing.Result;
+import com.google.zxing.client.android.CaptureActivity;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.ResultParser;
+
+/**
+ * Manufactures Android-specific handlers based on the barcode content's type.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ResultHandlerFactory {
+ private ResultHandlerFactory() {
+ }
+
+ public static ResultHandler makeResultHandler(CaptureActivity activity, Result rawResult) {
+ ParsedResult result = parseResult(rawResult);
+ switch (result.getType()) {
+ case ADDRESSBOOK:
+ return new AddressBookResultHandler(activity, result);
+ case EMAIL_ADDRESS:
+ return new EmailAddressResultHandler(activity, result);
+ case PRODUCT:
+ return new ProductResultHandler(activity, result, rawResult);
+ case URI:
+ return new URIResultHandler(activity, result);
+ case WIFI:
+ return new WifiResultHandler(activity, result);
+ case GEO:
+ return new GeoResultHandler(activity, result);
+ case TEL:
+ return new TelResultHandler(activity, result);
+ case SMS:
+ return new SMSResultHandler(activity, result);
+ case CALENDAR:
+ return new CalendarResultHandler(activity, result);
+ case ISBN:
+ return new ISBNResultHandler(activity, result, rawResult);
+ default:
+ return new TextResultHandler(activity, result, rawResult);
+ }
+ }
+
+ private static ParsedResult parseResult(Result rawResult) {
+ return ResultParser.parseResult(rawResult);
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java
new file mode 100644
index 0000000..f1c7a3c
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/SMSResultHandler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+import android.telephony.PhoneNumberUtils;
+
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.SMSParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles SMS addresses, offering a choice of composing a new SMS or MMS message.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class SMSResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_sms,
+ R.string.button_mms
+ };
+
+ public SMSResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttons.length;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ SMSParsedResult smsResult = (SMSParsedResult) getResult();
+ String number = smsResult.getNumbers()[0];
+ switch (index) {
+ case 0:
+ // Don't know of a way yet to express a SENDTO intent with multiple recipients
+ sendSMS(number, smsResult.getBody());
+ break;
+ case 1:
+ sendMMS(number, smsResult.getSubject(), smsResult.getBody());
+ break;
+ }
+ }
+
+ @Override
+ public CharSequence getDisplayContents() {
+ SMSParsedResult smsResult = (SMSParsedResult) getResult();
+ String[] rawNumbers = smsResult.getNumbers();
+ String[] formattedNumbers = new String[rawNumbers.length];
+ for (int i = 0; i < rawNumbers.length; i++) {
+ formattedNumbers[i] = PhoneNumberUtils.formatNumber(rawNumbers[i]);
+ }
+ StringBuilder contents = new StringBuilder(50);
+ ParsedResult.maybeAppend(formattedNumbers, contents);
+ ParsedResult.maybeAppend(smsResult.getSubject(), contents);
+ ParsedResult.maybeAppend(smsResult.getBody(), contents);
+ return contents.toString();
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_sms;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java
new file mode 100644
index 0000000..8816c2a
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/TelResultHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import net.foucry.pilldroid.R;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.TelParsedResult;
+
+import android.app.Activity;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Offers relevant actions for telephone numbers.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class TelResultHandler extends ResultHandler {
+ private static final int[] buttons = {
+ R.string.button_dial,
+ R.string.button_add_contact
+ };
+
+ public TelResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return buttons.length;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ TelParsedResult telResult = (TelParsedResult) getResult();
+ switch (index) {
+ case 0:
+ dialPhoneFromUri(telResult.getTelURI());
+ // When dialer comes up, it allows underlying display activity to continue or something,
+ // but app can't get camera in this state. Avoid issues by just quitting, only in the
+ // case of a phone number
+ getActivity().finish();
+ break;
+ case 1:
+ String[] numbers = new String[1];
+ numbers[0] = telResult.getNumber();
+ addPhoneOnlyContact(numbers, null);
+ break;
+ }
+ }
+
+ // Overriden so we can take advantage of Android's phone number hyphenation routines.
+ @Override
+ public CharSequence getDisplayContents() {
+ String contents = getResult().getDisplayResult();
+ contents = contents.replace("\r", "");
+ return PhoneNumberUtils.formatNumber(contents);
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_tel;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java
new file mode 100644
index 0000000..a5a9242
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/TextResultHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.Result;
+import com.google.zxing.client.result.ParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * This class handles TextParsedResult as well as unknown formats. It's the fallback handler.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class TextResultHandler extends ResultHandler {
+
+ private static final int[] buttons = {
+ R.string.button_web_search,
+ R.string.button_share_by_email,
+ R.string.button_share_by_sms,
+ R.string.button_custom_product_search,
+ };
+
+ public TextResultHandler(Activity activity, ParsedResult result, Result rawResult) {
+ super(activity, result, rawResult);
+ }
+
+ @Override
+ public int getButtonCount() {
+ return hasCustomProductSearch() ? buttons.length : buttons.length - 1;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ String text = getResult().getDisplayResult();
+ switch (index) {
+ case 0:
+ webSearch(text);
+ break;
+ case 1:
+ shareByEmail(text);
+ break;
+ case 2:
+ shareBySMS(text);
+ break;
+ case 3:
+ openURL(fillInCustomSearchURL(text));
+ break;
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_text;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java
new file mode 100644
index 0000000..2547462
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/URIResultHandler.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+
+import com.google.zxing.client.android.LocaleManager;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.URIParsedResult;
+
+import net.foucry.pilldroid.R;
+
+import java.util.Locale;
+
+/**
+ * Offers appropriate actions for URLS.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+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 = {
+ "otpauth:"
+ };
+
+ private static final int[] buttons = {
+ R.string.button_open_browser,
+ R.string.button_share_by_email,
+ R.string.button_share_by_sms,
+ R.string.button_search_book_contents,
+ };
+
+ public URIResultHandler(Activity activity, ParsedResult result) {
+ super(activity, result);
+ }
+
+ @Override
+ public int getButtonCount() {
+ if (LocaleManager.isBookSearchUrl(((URIParsedResult) getResult()).getURI())) {
+ return buttons.length;
+ }
+ return buttons.length - 1;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return buttons[index];
+ }
+
+ @Override
+ public Integer getDefaultButtonID() {
+ return 0;
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ URIParsedResult uriResult = (URIParsedResult) getResult();
+ String uri = uriResult.getURI();
+ switch (index) {
+ case 0:
+ openURL(uri);
+ break;
+ case 1:
+ shareByEmail(uri);
+ break;
+ case 2:
+ shareBySMS(uri);
+ break;
+ case 3:
+ searchBookContents(uri);
+ break;
+ }
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_uri;
+ }
+
+ @Override
+ public boolean areContentsSecure() {
+ URIParsedResult uriResult = (URIParsedResult) getResult();
+ String uri = uriResult.getURI().toLowerCase(Locale.ENGLISH);
+ for (String secure : SECURE_PROTOCOLS) {
+ if (uri.startsWith(secure)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java b/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java
new file mode 100644
index 0000000..28cdce5
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/WifiResultHandler.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2008 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.result;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.zxing.client.android.CaptureActivity;
+import com.google.zxing.client.android.wifi.WifiConfigManager;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.WifiParsedResult;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Handles wifi access information.
+ *
+ * @author Vikram Aggarwal
+ * @author Sean Owen
+ */
+public final class WifiResultHandler extends ResultHandler {
+
+ private static final String TAG = WifiResultHandler.class.getSimpleName();
+
+ private final CaptureActivity parent;
+
+ public WifiResultHandler(CaptureActivity activity, ParsedResult result) {
+ super(activity, result);
+ parent = activity;
+ }
+
+ @Override
+ public int getButtonCount() {
+ // We just need one button, and that is to configure the wireless. This could change in the future.
+ return 1;
+ }
+
+ @Override
+ public int getButtonText(int index) {
+ return R.string.button_wifi;
+ }
+
+ @Override
+ public void handleButtonPress(int index) {
+ if (index == 0) {
+ WifiParsedResult wifiResult = (WifiParsedResult) getResult();
+ WifiManager wifiManager = (WifiManager) getActivity().getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager == null) {
+ Log.w(TAG, "No WifiManager available from device");
+ return;
+ }
+ final Activity activity = getActivity();
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity.getApplicationContext(), R.string.wifi_changing_network, Toast.LENGTH_SHORT).show();
+ }
+ });
+ new WifiConfigManager(wifiManager).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, wifiResult);
+ parent.restartPreviewAfterDelay(0L);
+ }
+ }
+
+ // Display the name of the network and the network type to the user.
+ @Override
+ public CharSequence getDisplayContents() {
+ WifiParsedResult wifiResult = (WifiParsedResult) getResult();
+ return wifiResult.getSsid() + " (" + wifiResult.getNetworkEncryption() + ')';
+ }
+
+ @Override
+ public int getDisplayTitle() {
+ return R.string.result_wifi;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java
new file mode 100644
index 0000000..241abe2
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/BookResultInfoRetriever.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011 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.result.supplement;
+
+import android.content.Context;
+import android.widget.TextView;
+
+import com.google.zxing.client.android.HttpHelper;
+import com.google.zxing.client.android.LocaleManager;
+import com.google.zxing.client.android.history.HistoryManager;
+
+import net.foucry.pilldroid.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * @author Kamil Kaczmarczyk
+ * @author Sean Owen
+ */
+final class BookResultInfoRetriever extends SupplementalInfoRetriever {
+
+ private final String isbn;
+ private final String source;
+ private final Context context;
+
+ BookResultInfoRetriever(TextView textView, String isbn, HistoryManager historyManager, Context context) {
+ super(textView, historyManager);
+ this.isbn = isbn;
+ this.source = context.getString(R.string.msg_google_books);
+ this.context = context;
+ }
+
+ @Override
+ void retrieveSupplementalInfo() throws IOException {
+
+ CharSequence contents = HttpHelper.downloadViaHttp("https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn,
+ HttpHelper.ContentType.JSON);
+
+ if (contents.length() == 0) {
+ return;
+ }
+
+ String title;
+ String pages;
+ Collection authors = null;
+
+ try {
+
+ JSONObject topLevel = (JSONObject) new JSONTokener(contents.toString()).nextValue();
+ JSONArray items = topLevel.optJSONArray("items");
+ if (items == null || items.isNull(0)) {
+ return;
+ }
+
+ JSONObject volumeInfo = ((JSONObject) items.get(0)).getJSONObject("volumeInfo");
+ if (volumeInfo == null) {
+ return;
+ }
+
+ title = volumeInfo.optString("title");
+ pages = volumeInfo.optString("pageCount");
+
+ JSONArray authorsArray = volumeInfo.optJSONArray("authors");
+ if (authorsArray != null && !authorsArray.isNull(0)) {
+ authors = new ArrayList<>(authorsArray.length());
+ for (int i = 0; i < authorsArray.length(); i++) {
+ authors.add(authorsArray.getString(i));
+ }
+ }
+
+ } catch (JSONException e) {
+ throw new IOException(e);
+ }
+
+ Collection newTexts = new ArrayList<>();
+ maybeAddText(title, newTexts);
+ maybeAddTextSeries(authors, newTexts);
+ maybeAddText(pages == null || pages.isEmpty() ? null : pages + "pp.", newTexts);
+
+ String baseBookUri = "http://www.google." + LocaleManager.getBookSearchCountryTLD(context)
+ + "/search?tbm=bks&source=zxing&q=";
+
+ append(isbn, source, newTexts.toArray(new String[newTexts.size()]), baseBookUri + isbn);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java
new file mode 100644
index 0000000..84f3891
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/ProductResultInfoRetriever.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010 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.result.supplement;
+
+import android.content.Context;
+import android.text.Html;
+import android.widget.TextView;
+
+import com.google.zxing.client.android.HttpHelper;
+import com.google.zxing.client.android.LocaleManager;
+import com.google.zxing.client.android.history.HistoryManager;
+
+import net.foucry.pilldroid.R;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Retrieves product information from Google Product search.
+ *
+ * Please do not reuse this code. Using results in this way requires permission
+ * from Google, and that is not granted to users via this project.
+ *
+ * @author Sean Owen
+ */
+final class ProductResultInfoRetriever extends SupplementalInfoRetriever {
+
+ private static final Pattern[] PRODUCT_NAME_PRICE_PATTERNS = {
+ Pattern.compile(",event\\)\">([^<]+).+([^<]+) "),
+ Pattern.compile("owb63p\">([^<]+).+zdi3pb\">([^<]+)"),
+ };
+
+ private final String productID;
+ private final String source;
+ private final Context context;
+
+ ProductResultInfoRetriever(TextView textView, String productID, HistoryManager historyManager, Context context) {
+ super(textView, historyManager);
+ this.productID = productID;
+ this.source = context.getString(R.string.msg_google_product);
+ this.context = context;
+ }
+
+ @Override
+ void retrieveSupplementalInfo() throws IOException {
+
+ String encodedProductID = URLEncoder.encode(productID, "UTF-8");
+ String uri = "https://www.google." + LocaleManager.getProductSearchCountryTLD(context)
+ + "/m/products?ie=utf8&oe=utf8&scoring=p&source=zxing&q=" + encodedProductID;
+ CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.HTML);
+
+ for (Pattern p : PRODUCT_NAME_PRICE_PATTERNS) {
+ Matcher matcher = p.matcher(content);
+ if (matcher.find()) {
+ append(productID,
+ source,
+ new String[] { unescapeHTML(matcher.group(1)), unescapeHTML(matcher.group(2)) },
+ uri);
+ break;
+ }
+ }
+ }
+
+ private static String unescapeHTML(String raw) {
+ return Html.fromHtml(raw).toString();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java
new file mode 100644
index 0000000..6aa8533
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/SupplementalInfoRetriever.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010 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.result.supplement;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.URLSpan;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.RejectedExecutionException;
+
+import com.google.zxing.client.android.history.HistoryManager;
+import com.google.zxing.client.result.ISBNParsedResult;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.ProductParsedResult;
+import com.google.zxing.client.result.URIParsedResult;
+
+public abstract class SupplementalInfoRetriever extends AsyncTask {
+
+ private static final String TAG = "SupplementalInfo";
+
+ public static void maybeInvokeRetrieval(TextView textView,
+ ParsedResult result,
+ HistoryManager historyManager,
+ Context context) {
+ try {
+ if (result instanceof URIParsedResult) {
+ SupplementalInfoRetriever uriRetriever =
+ new URIResultInfoRetriever(textView, (URIParsedResult) result, historyManager, context);
+ uriRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ SupplementalInfoRetriever titleRetriever =
+ new TitleRetriever(textView, (URIParsedResult) result, historyManager);
+ titleRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else if (result instanceof ProductParsedResult) {
+ ProductParsedResult productParsedResult = (ProductParsedResult) result;
+ String productID = productParsedResult.getProductID();
+ SupplementalInfoRetriever productRetriever =
+ new ProductResultInfoRetriever(textView, productID, historyManager, context);
+ productRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else if (result instanceof ISBNParsedResult) {
+ String isbn = ((ISBNParsedResult) result).getISBN();
+ SupplementalInfoRetriever productInfoRetriever =
+ new ProductResultInfoRetriever(textView, isbn, historyManager, context);
+ productInfoRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ SupplementalInfoRetriever bookInfoRetriever =
+ new BookResultInfoRetriever(textView, isbn, historyManager, context);
+ bookInfoRetriever.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ } catch (RejectedExecutionException ree) {
+ // do nothing
+ }
+ }
+
+ private final WeakReference textViewRef;
+ private final WeakReference historyManagerRef;
+ private final Collection newContents;
+ private final Collection newHistories;
+
+ SupplementalInfoRetriever(TextView textView, HistoryManager historyManager) {
+ textViewRef = new WeakReference<>(textView);
+ historyManagerRef = new WeakReference<>(historyManager);
+ newContents = new ArrayList<>();
+ newHistories = new ArrayList<>();
+ }
+
+ @Override
+ public final Object doInBackground(Object... args) {
+ try {
+ retrieveSupplementalInfo();
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+ return null;
+ }
+
+ @Override
+ protected final void onPostExecute(Object arg) {
+ TextView textView = textViewRef.get();
+ if (textView != null) {
+ for (CharSequence content : newContents) {
+ textView.append(content);
+ }
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ HistoryManager historyManager = historyManagerRef.get();
+ if (historyManager != null) {
+ for (String[] text : newHistories) {
+ historyManager.addHistoryItemDetails(text[0], text[1]);
+ }
+ }
+ }
+
+ abstract void retrieveSupplementalInfo() throws IOException;
+
+ final void append(String itemID, String source, String[] newTexts, String linkURL) {
+
+ StringBuilder newTextCombined = new StringBuilder();
+
+ if (source != null) {
+ newTextCombined.append(source).append(' ');
+ }
+
+ int linkStart = newTextCombined.length();
+
+ boolean first = true;
+ for (String newText : newTexts) {
+ if (first) {
+ newTextCombined.append(newText);
+ first = false;
+ } else {
+ newTextCombined.append(" [");
+ newTextCombined.append(newText);
+ newTextCombined.append(']');
+ }
+ }
+
+ int linkEnd = newTextCombined.length();
+
+ String newText = newTextCombined.toString();
+ Spannable content = new SpannableString(newText + "\n\n");
+ if (linkURL != null) {
+ // Strangely, some Android browsers don't seem to register to handle HTTP:// or HTTPS://.
+ // Lower-case these as it should always be OK to lower-case these schemes.
+ if (linkURL.startsWith("HTTP://")) {
+ linkURL = "http" + linkURL.substring(4);
+ } else if (linkURL.startsWith("HTTPS://")) {
+ linkURL = "https" + linkURL.substring(5);
+ }
+ content.setSpan(new URLSpan(linkURL), linkStart, linkEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ newContents.add(content);
+ newHistories.add(new String[] {itemID, newText});
+ }
+
+ static void maybeAddText(String text, Collection texts) {
+ if (text != null && !text.isEmpty()) {
+ texts.add(text);
+ }
+ }
+
+ static void maybeAddTextSeries(Collection textSeries, Collection texts) {
+ if (textSeries != null && !textSeries.isEmpty()) {
+ boolean first = true;
+ StringBuilder authorsText = new StringBuilder();
+ for (String author : textSeries) {
+ if (first) {
+ first = false;
+ } else {
+ authorsText.append(", ");
+ }
+ authorsText.append(author);
+ }
+ texts.add(authorsText.toString());
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java
new file mode 100644
index 0000000..93467fc
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/TitleRetriever.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012 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.result.supplement;
+
+import android.text.Html;
+import android.widget.TextView;
+import com.google.zxing.client.android.HttpHelper;
+import com.google.zxing.client.android.history.HistoryManager;
+import com.google.zxing.client.result.URIParsedResult;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Retrieves the title of a web page as supplemental info.
+ *
+ * @author Sean Owen
+ */
+final class TitleRetriever extends SupplementalInfoRetriever {
+
+ private static final Pattern TITLE_PATTERN = Pattern.compile("([^<]+)");
+ private static final int MAX_TITLE_LEN = 100;
+
+ private final String httpUrl;
+
+ TitleRetriever(TextView textView, URIParsedResult result, HistoryManager historyManager) {
+ super(textView, historyManager);
+ this.httpUrl = result.getURI();
+ }
+
+ @Override
+ void retrieveSupplementalInfo() {
+ CharSequence contents;
+ try {
+ contents = HttpHelper.downloadViaHttp(httpUrl, HttpHelper.ContentType.HTML, 4096);
+ } catch (IOException ioe) {
+ // ignore this
+ return;
+ }
+ if (contents != null && contents.length() > 0) {
+ Matcher m = TITLE_PATTERN.matcher(contents);
+ if (m.find()) {
+ String title = m.group(1);
+ if (title != null && !title.isEmpty()) {
+ title = Html.fromHtml(title).toString();
+ if (title.length() > MAX_TITLE_LEN) {
+ title = title.substring(0, MAX_TITLE_LEN) + "...";
+ }
+ append(httpUrl, null, new String[] {title}, httpUrl);
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java b/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java
new file mode 100644
index 0000000..0f6df6f
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/result/supplement/URIResultInfoRetriever.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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.result.supplement;
+
+import android.content.Context;
+import android.widget.TextView;
+
+import com.google.zxing.client.android.HttpHelper;
+import com.google.zxing.client.android.history.HistoryManager;
+import com.google.zxing.client.result.URIParsedResult;
+
+import net.foucry.pilldroid.R;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+final class URIResultInfoRetriever extends SupplementalInfoRetriever {
+
+ private static final int MAX_REDIRECTS = 5;
+
+ private final URIParsedResult result;
+ private final String redirectString;
+
+ URIResultInfoRetriever(TextView textView, URIParsedResult result, HistoryManager historyManager, Context context) {
+ super(textView, historyManager);
+ redirectString = context.getString(R.string.msg_redirect);
+ this.result = result;
+ }
+
+ @Override
+ void retrieveSupplementalInfo() throws IOException {
+ URI oldURI;
+ try {
+ oldURI = new URI(result.getURI());
+ } catch (URISyntaxException ignored) {
+ return;
+ }
+ URI newURI = HttpHelper.unredirect(oldURI);
+ int count = 0;
+ while (count++ < MAX_REDIRECTS && !oldURI.equals(newURI)) {
+ append(result.getDisplayResult(),
+ null,
+ new String[] { redirectString + " : " + newURI },
+ newURI.toString());
+ oldURI = newURI;
+ newURI = HttpHelper.unredirect(newURI);
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java b/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java
new file mode 100644
index 0000000..94bde9b
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/AppInfo.java
@@ -0,0 +1,65 @@
+/*
+ * 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.share;
+
+import android.graphics.drawable.Drawable;
+
+final class AppInfo implements Comparable {
+
+ private final String packageName;
+ private final String label;
+ private final Drawable icon;
+
+ AppInfo(String packageName, String label, Drawable icon) {
+ this.packageName = packageName;
+ this.label = label;
+ this.icon = icon;
+ }
+
+ String getPackageName() {
+ return packageName;
+ }
+
+ Drawable getIcon() {
+ return icon;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+
+ @Override
+ public int compareTo(AppInfo another) {
+ return label.compareTo(another.label);
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof AppInfo)) {
+ return false;
+ }
+ AppInfo another = (AppInfo) other;
+ return label.equals(another.label);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java b/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java
new file mode 100644
index 0000000..778beaf
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/AppPickerActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2009 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.share;
+
+import android.app.ListActivity;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.ListView;
+
+import java.util.List;
+
+public final class AppPickerActivity extends ListActivity {
+
+ private AsyncTask> backgroundTask;
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ backgroundTask = new LoadPackagesAsyncTask(this);
+ backgroundTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ protected void onPause() {
+ AsyncTask,?,?> task = backgroundTask;
+ if (task != null) {
+ task.cancel(true);
+ backgroundTask = null;
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View view, int position, long id) {
+ Adapter adapter = getListAdapter();
+ if (position >= 0 && position < adapter.getCount()) {
+ String packageName = ((AppInfo) adapter.getItem(position)).getPackageName();
+ Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra("url", "market://details?id=" + packageName); // Browser.BookmarkColumns.URL
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java b/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java
new file mode 100644
index 0000000..b95a6ee
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/BookmarkAdapter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2011 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.share;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * A custom adapter designed to fetch bookmarks from a cursor. Before Honeycomb we used
+ * SimpleCursorAdapter, but it assumes the existence of an _id column, and the bookmark schema was
+ * rewritten for HC without one. This caused the app to crash, hence this new class, which is
+ * forwards and backwards compatible.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class BookmarkAdapter extends BaseAdapter {
+ private final Context context;
+ private final Cursor cursor;
+
+ BookmarkAdapter(Context context, Cursor cursor) {
+ this.context = context;
+ this.cursor = cursor;
+ }
+
+ @Override
+ public int getCount() {
+ return cursor.isClosed() ? 0 : cursor.getCount();
+ }
+
+ @Override
+ public Object getItem(int index) {
+ // Not used, so no point in retrieving it.
+ return null;
+ }
+
+ @Override
+ public long getItemId(int index) {
+ return index;
+ }
+
+ @Override
+ public View getView(int index, View view, ViewGroup viewGroup) {
+ View layout;
+ if (view instanceof LinearLayout) {
+ layout = view;
+ } else {
+ LayoutInflater factory = LayoutInflater.from(context);
+ layout = factory.inflate(R.layout.bookmark_picker_list_item, viewGroup, false);
+ }
+
+ if (!cursor.isClosed()) {
+ cursor.moveToPosition(index);
+ CharSequence title = cursor.getString(BookmarkPickerActivity.TITLE_COLUMN);
+ ((TextView) layout.findViewById(R.id.bookmark_title)).setText(title);
+ CharSequence url = cursor.getString(BookmarkPickerActivity.URL_COLUMN);
+ ((TextView) layout.findViewById(R.id.bookmark_url)).setText(url);
+ } // Otherwise... just don't update as the object is shutting down
+ return layout;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java b/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java
new file mode 100644
index 0000000..c4cc1f3
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/BookmarkPickerActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2008 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.share;
+
+import android.app.ListActivity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * This class is only needed because I can't successfully send an ACTION_PICK intent to
+ * com.android.browser.BrowserBookmarksPage. It can go away if that starts working in the future.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class BookmarkPickerActivity extends ListActivity {
+
+ private static final String TAG = BookmarkPickerActivity.class.getSimpleName();
+
+ private static final String[] BOOKMARK_PROJECTION = {
+ "title", // Browser.BookmarkColumns.TITLE
+ "url", // Browser.BookmarkColumns.URL
+ };
+ // Copied from android.provider.Browser.BOOKMARKS_URI:
+ private static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
+
+ static final int TITLE_COLUMN = 0;
+ static final int URL_COLUMN = 1;
+
+ private static final String BOOKMARK_SELECTION = "bookmark = 1 AND url IS NOT NULL";
+
+ private Cursor cursor;
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ cursor = getContentResolver().query(BOOKMARKS_URI, BOOKMARK_PROJECTION,
+ BOOKMARK_SELECTION, null, null);
+ if (cursor == null) {
+ Log.w(TAG, "No cursor returned for bookmark query");
+ finish();
+ return;
+ }
+ setListAdapter(new BookmarkAdapter(this, cursor));
+ }
+
+ @Override
+ protected void onPause() {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View view, int position, long id) {
+ if (!cursor.isClosed() && cursor.moveToPosition(position)) {
+ Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra("title", cursor.getString(TITLE_COLUMN)); // Browser.BookmarkColumns.TITLE
+ intent.putExtra("url", cursor.getString(URL_COLUMN)); // Browser.BookmarkColumns.URL
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java b/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java
new file mode 100644
index 0000000..a49f2a0
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/LoadPackagesAsyncTask.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2009 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.share;
+
+import android.app.ListActivity;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageItemInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import net.foucry.pilldroid.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Loads a list of packages installed on the device asynchronously.
+ *
+ * @author Sean Owen
+ */
+final class LoadPackagesAsyncTask extends AsyncTask> {
+
+ private static final String[] PKG_PREFIX_WHITELIST = {
+ "com.google.android.apps.",
+ };
+ private static final String[] PKG_PREFIX_BLACKLIST = {
+ "com.android.",
+ "android",
+ "com.google.android.",
+ "com.htc",
+ };
+
+ private final ListActivity activity;
+
+ LoadPackagesAsyncTask(ListActivity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ protected List doInBackground(Object... objects) {
+ List labelsPackages = new ArrayList<>();
+ PackageManager packageManager = activity.getPackageManager();
+ Iterable appInfos = packageManager.getInstalledApplications(0);
+ for (PackageItemInfo appInfo : appInfos) {
+ String packageName = appInfo.packageName;
+ if (!isHidden(packageName)) {
+ CharSequence label = appInfo.loadLabel(packageManager);
+ Drawable icon = appInfo.loadIcon(packageManager);
+ if (label != null) {
+ labelsPackages.add(new AppInfo(packageName, label.toString(), icon));
+ }
+ }
+ }
+ Collections.sort(labelsPackages);
+ return labelsPackages;
+ }
+
+ private static boolean isHidden(String packageName) {
+ if (packageName == null) {
+ return true;
+ }
+ for (String prefix : PKG_PREFIX_WHITELIST) {
+ if (packageName.startsWith(prefix)) {
+ return false;
+ }
+ }
+ for (String prefix : PKG_PREFIX_BLACKLIST) {
+ if (packageName.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void onPostExecute(final List results) {
+ ListAdapter listAdapter = new ArrayAdapter(activity,
+ R.layout.app_picker_list_item,
+ R.id.app_picker_list_item_label,
+ results) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ Drawable icon = results.get(position).getIcon();
+ if (icon != null) {
+ ((ImageView) view.findViewById(R.id.app_picker_list_item_icon)).setImageDrawable(icon);
+ }
+ return view;
+ }
+ };
+ activity.setListAdapter(listAdapter);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java b/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java
new file mode 100644
index 0000000..d779d20
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/share/ShareActivity.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2008 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.share;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.client.android.Contents;
+import com.google.zxing.client.android.Intents;
+import com.google.zxing.client.android.clipboard.ClipboardInterface;
+
+import net.foucry.pilldroid.R;
+
+/**
+ * Barcode Scanner can share data like contacts and bookmarks by displaying a QR Code on screen,
+ * such that another user can scan the barcode with their phone.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ShareActivity extends Activity {
+
+ private static final String TAG = ShareActivity.class.getSimpleName();
+
+ private static final int PICK_BOOKMARK = 0;
+ private static final int PICK_CONTACT = 1;
+ private static final int PICK_APP = 2;
+
+ private View clipboardButton;
+
+ private final View.OnClickListener contactListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ startActivityForResult(intent, PICK_CONTACT);
+ }
+ };
+
+ private final View.OnClickListener bookmarkListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.setClassName(ShareActivity.this, BookmarkPickerActivity.class.getName());
+ startActivityForResult(intent, PICK_BOOKMARK);
+ }
+ };
+
+ private final View.OnClickListener appListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.setClassName(ShareActivity.this, AppPickerActivity.class.getName());
+ startActivityForResult(intent, PICK_APP);
+ }
+ };
+
+ private final View.OnClickListener clipboardListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Should always be true, because we grey out the clipboard button in onResume() if it's empty
+ CharSequence text = ClipboardInterface.getText(ShareActivity.this);
+ if (text != null) {
+ launchSearch(text.toString());
+ }
+ }
+ };
+
+ private final View.OnKeyListener textListener = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
+ String text = ((TextView) view).getText().toString();
+ if (text != null && !text.isEmpty()) {
+ launchSearch(text);
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
+ private void launchSearch(String text) {
+ Intent intent = new Intent(Intents.Encode.ACTION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra(Intents.Encode.TYPE, Contents.Type.TEXT);
+ intent.putExtra(Intents.Encode.DATA, text);
+ intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString());
+ startActivity(intent);
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.share);
+
+ findViewById(R.id.share_contact_button).setOnClickListener(contactListener);
+ if (Build.VERSION.SDK_INT >= 23) { // Marshmallow / 6.0
+ // Can't access bookmarks in 6.0+
+ findViewById(R.id.share_bookmark_button).setEnabled(false);
+ } else {
+ findViewById(R.id.share_bookmark_button).setOnClickListener(bookmarkListener);
+ }
+ findViewById(R.id.share_app_button).setOnClickListener(appListener);
+ clipboardButton = findViewById(R.id.share_clipboard_button);
+ clipboardButton.setOnClickListener(clipboardListener);
+ findViewById(R.id.share_text_view).setOnKeyListener(textListener);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ clipboardButton.setEnabled(ClipboardInterface.hasText(this));
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (resultCode == RESULT_OK) {
+ switch (requestCode) {
+ case PICK_BOOKMARK:
+ case PICK_APP:
+ showTextAsBarcode(intent.getStringExtra("url")); // Browser.BookmarkColumns.URL
+ break;
+ case PICK_CONTACT:
+ // Data field is content://contacts/people/984
+ showContactAsBarcode(intent.getData());
+ break;
+ }
+ }
+ }
+
+ private void showTextAsBarcode(String text) {
+ Log.i(TAG, "Showing text as barcode: " + text);
+ if (text == null) {
+ return; // Show error?
+ }
+ Intent intent = new Intent(Intents.Encode.ACTION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra(Intents.Encode.TYPE, Contents.Type.TEXT);
+ intent.putExtra(Intents.Encode.DATA, text);
+ intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString());
+ startActivity(intent);
+ }
+
+ /**
+ * Takes a contact Uri and does the necessary database lookups to retrieve that person's info,
+ * then sends an Encode intent to render it as a QR Code.
+ *
+ * @param contactUri A Uri of the form content://contacts/people/17
+ */
+ private void showContactAsBarcode(Uri contactUri) {
+ Log.i(TAG, "Showing contact URI as barcode: " + contactUri);
+ if (contactUri == null) {
+ return; // Show error?
+ }
+ ContentResolver resolver = getContentResolver();
+
+ Cursor cursor;
+ try {
+ // We're seeing about six reports a week of this exception although I don't understand why.
+ cursor = resolver.query(contactUri, null, null, null, null);
+ } catch (IllegalArgumentException ignored) {
+ return;
+ }
+ if (cursor == null) {
+ return;
+ }
+
+ String id;
+ String name;
+ boolean hasPhone;
+ try {
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+
+ id = cursor.getString(cursor.getColumnIndex(BaseColumns._ID));
+ name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
+ hasPhone = cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0;
+
+
+ } finally {
+ cursor.close();
+ }
+
+ // Don't require a name to be present, this contact might be just a phone number.
+ Bundle bundle = new Bundle();
+ if (name != null && !name.isEmpty()) {
+ bundle.putString(ContactsContract.Intents.Insert.NAME, massageContactData(name));
+ }
+
+ if (hasPhone) {
+ Cursor phonesCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ null,
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + '=' + id,
+ null,
+ null);
+ if (phonesCursor != null) {
+ try {
+ int foundPhone = 0;
+ int phonesNumberColumn = phonesCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
+ int phoneTypeColumn = phonesCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
+ while (phonesCursor.moveToNext() && foundPhone < Contents.PHONE_KEYS.length) {
+ String number = phonesCursor.getString(phonesNumberColumn);
+ if (number != null && !number.isEmpty()) {
+ bundle.putString(Contents.PHONE_KEYS[foundPhone], massageContactData(number));
+ }
+ int type = phonesCursor.getInt(phoneTypeColumn);
+ bundle.putInt(Contents.PHONE_TYPE_KEYS[foundPhone], type);
+ foundPhone++;
+ }
+ } finally {
+ phonesCursor.close();
+ }
+ }
+ }
+
+ Cursor methodsCursor = resolver.query(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
+ null,
+ ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID + '=' + id,
+ null,
+ null);
+ if (methodsCursor != null) {
+ try {
+ if (methodsCursor.moveToNext()) {
+ String data = methodsCursor.getString(
+ methodsCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS));
+ if (data != null && !data.isEmpty()) {
+ bundle.putString(ContactsContract.Intents.Insert.POSTAL, massageContactData(data));
+ }
+ }
+ } finally {
+ methodsCursor.close();
+ }
+ }
+
+ Cursor emailCursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,
+ null,
+ ContactsContract.CommonDataKinds.Email.CONTACT_ID + '=' + id,
+ null,
+ null);
+ if (emailCursor != null) {
+ try {
+ int foundEmail = 0;
+ int emailColumn = emailCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
+ while (emailCursor.moveToNext() && foundEmail < Contents.EMAIL_KEYS.length) {
+ String email = emailCursor.getString(emailColumn);
+ if (email != null && !email.isEmpty()) {
+ bundle.putString(Contents.EMAIL_KEYS[foundEmail], massageContactData(email));
+ }
+ foundEmail++;
+ }
+ } finally {
+ emailCursor.close();
+ }
+ }
+
+ Intent intent = new Intent(Intents.Encode.ACTION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra(Intents.Encode.TYPE, Contents.Type.CONTACT);
+ intent.putExtra(Intents.Encode.DATA, bundle);
+ intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString());
+
+ Log.i(TAG, "Sending bundle for encoding: " + bundle);
+ startActivity(intent);
+ }
+
+ private static String massageContactData(String data) {
+ // For now -- make sure we don't put newlines in shared contact data. It messes up
+ // any known encoding of contact data. Replace with space.
+ if (data.indexOf('\n') >= 0) {
+ data = data.replace("\n", " ");
+ }
+ if (data.indexOf('\r') >= 0) {
+ data = data.replace("\r", " ");
+ }
+ return data;
+ }
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java b/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java
new file mode 100644
index 0000000..809e3c6
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/wifi/NetworkType.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2011 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.wifi;
+
+enum NetworkType {
+
+ WEP,
+ WPA,
+ NO_PASSWORD;
+
+ static NetworkType forIntentValue(String networkTypeString) {
+ if (networkTypeString == null) {
+ return NO_PASSWORD;
+ }
+ if ("WPA".equals(networkTypeString)) {
+ return WPA;
+ }
+ if ("WEP".equals(networkTypeString)) {
+ return WEP;
+ }
+ if ("nopass".equals(networkTypeString)) {
+ return NO_PASSWORD;
+ }
+ throw new IllegalArgumentException(networkTypeString);
+ }
+
+}
diff --git a/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java b/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java
new file mode 100644
index 0000000..ba11f90
--- /dev/null
+++ b/app/src/main/java/com/google/zxing/client/android/wifi/WifiConfigManager.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2011 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.wifi;
+
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+import com.google.zxing.client.result.WifiParsedResult;
+
+/**
+ * @author Vikram Aggarwal
+ * @author Sean Owen
+ */
+public final class WifiConfigManager extends AsyncTask {
+
+ private static final String TAG = WifiConfigManager.class.getSimpleName();
+
+ private static final Pattern HEX_DIGITS = Pattern.compile("[0-9A-Fa-f]+");
+
+ private final WifiManager wifiManager;
+
+ public WifiConfigManager(WifiManager wifiManager) {
+ this.wifiManager = wifiManager;
+ }
+
+ @Override
+ protected Object doInBackground(WifiParsedResult... args) {
+ WifiParsedResult theWifiResult = args[0];
+ // Start WiFi, otherwise nothing will work
+ if (!wifiManager.isWifiEnabled()) {
+ Log.i(TAG, "Enabling wi-fi...");
+ if (wifiManager.setWifiEnabled(true)) {
+ Log.i(TAG, "Wi-fi enabled");
+ } else {
+ Log.w(TAG, "Wi-fi could not be enabled!");
+ return null;
+ }
+ // This happens very quickly, but need to wait for it to enable. A little busy wait?
+ int count = 0;
+ while (!wifiManager.isWifiEnabled()) {
+ if (count >= 10) {
+ Log.i(TAG, "Took too long to enable wi-fi, quitting");
+ return null;
+ }
+ Log.i(TAG, "Still waiting for wi-fi to enable...");
+ try {
+ Thread.sleep(1000L);
+ } catch (InterruptedException ie) {
+ // continue
+ }
+ count++;
+ }
+ }
+ String networkTypeString = theWifiResult.getNetworkEncryption();
+ NetworkType networkType;
+ try {
+ networkType = NetworkType.forIntentValue(networkTypeString);
+ } catch (IllegalArgumentException ignored) {
+ Log.w(TAG, "Bad network type; see NetworkType values: " + networkTypeString);
+ return null;
+ }
+ if (networkType == NetworkType.NO_PASSWORD) {
+ changeNetworkUnEncrypted(wifiManager, theWifiResult);
+ } else {
+ String password = theWifiResult.getPassword();
+ if (password != null && !password.isEmpty()) {
+ if (networkType == NetworkType.WEP) {
+ changeNetworkWEP(wifiManager, theWifiResult);
+ } else if (networkType == NetworkType.WPA) {
+ changeNetworkWPA(wifiManager, theWifiResult);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Update the network: either create a new network or modify an existing network
+ * @param config the new network configuration
+ */
+ private static void updateNetwork(WifiManager wifiManager, WifiConfiguration config) {
+ Integer foundNetworkID = findNetworkInExistingConfig(wifiManager, config.SSID);
+ if (foundNetworkID != null) {
+ Log.i(TAG, "Removing old configuration for network " + config.SSID);
+ wifiManager.removeNetwork(foundNetworkID);
+ wifiManager.saveConfiguration();
+ }
+ int networkId = wifiManager.addNetwork(config);
+ if (networkId >= 0) {
+ // Try to disable the current network and start a new one.
+ if (wifiManager.enableNetwork(networkId, true)) {
+ Log.i(TAG, "Associating to network " + config.SSID);
+ wifiManager.saveConfiguration();
+ } else {
+ Log.w(TAG, "Failed to enable network " + config.SSID);
+ }
+ } else {
+ Log.w(TAG, "Unable to add network " + config.SSID);
+ }
+ }
+
+ private static WifiConfiguration changeNetworkCommon(WifiParsedResult wifiResult) {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedAuthAlgorithms.clear();
+ config.allowedGroupCiphers.clear();
+ config.allowedKeyManagement.clear();
+ config.allowedPairwiseCiphers.clear();
+ config.allowedProtocols.clear();
+ // Android API insists that an ascii SSID must be quoted to be correctly handled.
+ config.SSID = quoteNonHex(wifiResult.getSsid());
+ config.hiddenSSID = wifiResult.isHidden();
+ return config;
+ }
+
+ // Adding a WEP network
+ private static void changeNetworkWEP(WifiManager wifiManager, WifiParsedResult wifiResult) {
+ WifiConfiguration config = changeNetworkCommon(wifiResult);
+ config.wepKeys[0] = quoteNonHex(wifiResult.getPassword(), 10, 26, 58);
+ config.wepTxKeyIndex = 0;
+ config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
+ updateNetwork(wifiManager, config);
+ }
+
+ // Adding a WPA or WPA2 network
+ private static void changeNetworkWPA(WifiManager wifiManager, WifiParsedResult wifiResult) {
+ WifiConfiguration config = changeNetworkCommon(wifiResult);
+ // Hex passwords that are 64 bits long are not to be quoted.
+ config.preSharedKey = quoteNonHex(wifiResult.getPassword(), 64);
+ config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+ config.allowedProtocols.set(WifiConfiguration.Protocol.WPA); // For WPA
+ config.allowedProtocols.set(WifiConfiguration.Protocol.RSN); // For WPA2
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+ config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
+ config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+ config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+ updateNetwork(wifiManager, config);
+ }
+
+ // Adding an open, unsecured network
+ private static void changeNetworkUnEncrypted(WifiManager wifiManager, WifiParsedResult wifiResult) {
+ WifiConfiguration config = changeNetworkCommon(wifiResult);
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ updateNetwork(wifiManager, config);
+ }
+
+ private static Integer findNetworkInExistingConfig(WifiManager wifiManager, String ssid) {
+ Iterable existingConfigs = wifiManager.getConfiguredNetworks();
+ if (existingConfigs != null) {
+ for (WifiConfiguration existingConfig : existingConfigs) {
+ String existingSSID = existingConfig.SSID;
+ if (existingSSID != null && existingSSID.equals(ssid)) {
+ return existingConfig.networkId;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static String quoteNonHex(String value, int... allowedLengths) {
+ return isHexOfLength(value, allowedLengths) ? value : convertToQuotedString(value);
+ }
+
+ /**
+ * Encloses the incoming string inside double quotes, if it isn't already quoted.
+ * @param s the input string
+ * @return a quoted string, of the form "input". If the input string is null, it returns null
+ * as well.
+ */
+ private static String convertToQuotedString(String s) {
+ if (s == null || s.isEmpty()) {
+ return null;
+ }
+ // If already quoted, return as-is
+ if (s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"') {
+ return s;
+ }
+ return '\"' + s + '\"';
+ }
+
+ /**
+ * @param value input to check
+ * @param allowedLengths allowed lengths, if any
+ * @return true if value is a non-null, non-empty string of hex digits, and if allowed lengths are given, has
+ * an allowed length
+ */
+ private static boolean isHexOfLength(CharSequence value, int... allowedLengths) {
+ if (value == null || !HEX_DIGITS.matcher(value).matches()) {
+ return false;
+ }
+ if (allowedLengths.length == 0) {
+ return true;
+ }
+ for (int length : allowedLengths) {
+ if (value.length() == length) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/app/src/main/java/net/foucry/pilldroid/DBMedoc.java b/app/src/main/java/net/foucry/pilldroid/DBMedoc.java
index bdd83ba..e15488f 100644
--- a/app/src/main/java/net/foucry/pilldroid/DBMedoc.java
+++ b/app/src/main/java/net/foucry/pilldroid/DBMedoc.java
@@ -12,8 +12,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import net.foucry.pilldroid.Medicament;
-
/**
* Created by jfoucry on 5/25/16.
*/
@@ -27,15 +25,15 @@ public class DBMedoc extends SQLiteOpenHelper{
private final Context myContext;
private static final String TABLE_NAME = "medicaments";
- private static final String MEDOC_ID = "id";
+// private static final String MEDOC_ID = "id";
private static final String MEDOC_CIS = "cis";
private static final String MEDOC_CIP13 = "cip13";
- private static final String MEDOC_CIP7 = "cip7";
+// private static final String MEDOC_CIP7 = "cip7";
private static final String MEDOC_ADMIN = "mode_administration";
private static final String MEDOC_NOM = "nom";
private static final String MEDOC_PRES = "presentation";
- private static final String[] COLUMNS_NAMES = {MEDOC_ID, MEDOC_CIS, MEDOC_CIP13, MEDOC_CIP7, MEDOC_ADMIN, MEDOC_NOM, MEDOC_PRES};
+ private static final String[] COLUMNS_NAMES = {MEDOC_CIS, MEDOC_CIP13, MEDOC_ADMIN, MEDOC_NOM, MEDOC_PRES};
public DBMedoc(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -140,8 +138,8 @@ public class DBMedoc extends SQLiteOpenHelper{
// Build query
Cursor cursor = db.query(TABLE_NAME, // Which table
COLUMNS_NAMES, // column names
- " cip13 = ?", // selections
- new String[]{String.valueOf(cip13)}, // selections args
+ " cip13 =?", // selections
+ new String[]{cip13}, // selections args
null, // group by
null, // having
null, // order by
@@ -152,16 +150,22 @@ public class DBMedoc extends SQLiteOpenHelper{
// Build medicament object
Medicament medicament = new Medicament();
- medicament.setId(Integer.parseInt(cursor.getString(0)));
- medicament.setCis(cursor.getString(1));
- medicament.setCip13(cursor.getString(2));
+ // medicament.setId(Integer.parseInt(cursor.getString(0)));
+ medicament.setCis(cursor.getString(0));
+ medicament.setCip13(cursor.getString(1));
+ medicament.setMode_administration(cursor.getString(2));
medicament.setNom(cursor.getString(3));
- medicament.setMode_administration(cursor.getString(4));
- medicament.setPresentation(cursor.getString(5));
- medicament.setStock(Double.parseDouble(cursor.getString(6)));
- medicament.setPrise(Double.parseDouble(cursor.getString(7)));
- medicament.setWarnThreshold(Integer.parseInt(cursor.getString(8)));
- medicament.setAlertThreshold(Integer.parseInt(cursor.getString(9)));
+ medicament.setPresentation(cursor.getString(4));
+ /*medicament.setStock(Double.parseDouble(cursor.getString(5)));
+ medicament.setPrise(Double.parseDouble(cursor.getString(6)));
+ medicament.setWarnThreshold(Integer.parseInt(cursor.getString(7)));
+ medicament.setAlertThreshold(Integer.parseInt(cursor.getString(8)));*/
+
+ // Set default values
+ medicament.setStock(0);
+ medicament.setPrise(0);
+ medicament.setWarnThreshold(14);
+ medicament.setAlertThreshold(7);
// Log
Log.d(MedicamentListActivity.Constants.TAG, "getDrug(" + cip13 + ")" + medicament.toString());
diff --git a/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java b/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java
index 5d357fc..0ee95fc 100644
--- a/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java
+++ b/app/src/main/java/net/foucry/pilldroid/MedicamentListActivity.java
@@ -1,15 +1,14 @@
package net.foucry.pilldroid;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
-import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -17,12 +16,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
+import android.widget.Toast;
-
-import net.foucry.pilldroid.Medicament;
-import net.foucry.pilldroid.dummy.DummyContent;
-import static net.foucry.pilldroid.UtilDate.*;
-import static net.foucry.pilldroid.Utils.*;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
@@ -30,6 +25,9 @@ import java.util.List;
import java.util.Locale;
import java.util.Random;
+import static net.foucry.pilldroid.UtilDate.date2String;
+import static net.foucry.pilldroid.Utils.doubleRandomInclusive;
+
/**
* An activity representing a list of Medicaments. This activity
* has different presentations for handset and tablet-size devices. On
@@ -53,6 +51,8 @@ public class MedicamentListActivity extends AppCompatActivity {
}
private static DBHelper dbHelper;
+ private static DBMedoc dbMedoc;
+
private SimpleCursorAdapter drugAdapter;
private List medicaments;
@@ -62,6 +62,7 @@ public class MedicamentListActivity extends AppCompatActivity {
setContentView(R.layout.activity_medicament_list);
dbHelper = new DBHelper(this);
+ dbMedoc = new DBMedoc(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
@@ -70,14 +71,18 @@ public class MedicamentListActivity extends AppCompatActivity {
toolbar.setTitle(getTitle());
}
- FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ /*FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- Snackbar.make(view, "Will be used to add a drug to the list", Snackbar.LENGTH_LONG)
- .setAction("Action", null).show();
+ *//* Snackbar.make(view, "Will be used to add a drug to the list", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show(); *//*
+ Intent intent = new Intent("com.google.zxing.client.android.SCAN");
+ intent.putExtra("SCAN_MODE", "CODE_128");
+ //intent.putExtra("SCAN_FORMATS", "EAN_13,DATA_MATRIX");
+ startActivityForResult(intent, 0);
}
- });
+ });*/
if (DEMO) {
@@ -142,6 +147,57 @@ public class MedicamentListActivity extends AppCompatActivity {
}
}
+ public void scanNow(View view) {
+ Intent intent = new Intent("com.google.zxing.client.android.SCAN");
+ //intent.putExtra("SCAN_MODE", "CODE_128");
+ intent.putExtra("SCAN_FORMATS", "CODE_18,DATA_MATRIX");
+ startActivityForResult(intent, 0);
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ Context context = getApplicationContext();
+ String cip13 = null;
+ if (requestCode == 0) {
+ if (resultCode == RESULT_OK) {
+ String contents = intent.getStringExtra("SCAN_RESULT");
+ String format = intent.getStringExtra("SCAN_RESULT_FORMAT");
+ Log.i(Constants.TAG, "Format:" + format);
+ Log.i(Constants.TAG, "Content:" + contents);
+
+ // Handle successful scan
+ if (format.equals("CODE_128")) { //CODE_128
+ cip13 = contents;
+ } else
+ {
+ cip13 = contents.substring(4,17);
+ }
+
+ dbMedoc.openDatabase();
+ Medicament scannedMedoc = dbMedoc.getMedocByCIP13(cip13);
+ dbMedoc.close();
+
+ if (scannedMedoc != null) {
+ Toast.makeText(context, "Medicament found in database", Toast.LENGTH_LONG).show();
+ } else
+ {
+ AlertDialog.Builder dlg = new AlertDialog.Builder(this);
+ dlg.setTitle(context.getString(R.string.app_name));
+ dlg.setMessage(context.getString(R.string.msgNotFound));
+ dlg.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // nothing to do to just dismiss dialog
+ }
+ });
+
+ }
+ } else if (resultCode == RESULT_CANCELED) {
+ // Handle cancel
+ Toast.makeText(context, "Scan annulé", Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
recyclerView.addItemDecoration(new SimpleDividerItemDecoration(getApplicationContext()));
recyclerView.setAdapter(new SimpleItemRecyclerViewAdapter(medicaments));
diff --git a/app/src/main/res/drawable/share_via_barcode.png b/app/src/main/res/drawable/share_via_barcode.png
new file mode 100644
index 0000000..56c3449
Binary files /dev/null and b/app/src/main/res/drawable/share_via_barcode.png differ
diff --git a/app/src/main/res/layout/activity_medicament_list.xml b/app/src/main/res/layout/activity_medicament_list.xml
index a7aa1fb..33ff41a 100644
--- a/app/src/main/res/layout/activity_medicament_list.xml
+++ b/app/src/main/res/layout/activity_medicament_list.xml
@@ -36,6 +36,7 @@
android:layout_height="60dp"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
+ android:onClick="scanNow"
android:src="@android:drawable/ic_input_add"
android:adjustViewBounds="true"
android:clickable="false"
diff --git a/app/src/main/res/layout/app_picker_list_item.xml b/app/src/main/res/layout/app_picker_list_item.xml
new file mode 100644
index 0000000..0f68618
--- /dev/null
+++ b/app/src/main/res/layout/app_picker_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/bookmark_picker_list_item.xml b/app/src/main/res/layout/bookmark_picker_list_item.xml
new file mode 100644
index 0000000..95398b4
--- /dev/null
+++ b/app/src/main/res/layout/bookmark_picker_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/capture.xml b/app/src/main/res/layout/capture.xml
new file mode 100644
index 0000000..658e0f3
--- /dev/null
+++ b/app/src/main/res/layout/capture.xml
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/encode.xml b/app/src/main/res/layout/encode.xml
new file mode 100644
index 0000000..120935f
--- /dev/null
+++ b/app/src/main/res/layout/encode.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/help.xml b/app/src/main/res/layout/help.xml
new file mode 100644
index 0000000..786a624
--- /dev/null
+++ b/app/src/main/res/layout/help.xml
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/history_list_item.xml b/app/src/main/res/layout/history_list_item.xml
new file mode 100644
index 0000000..0c060b4
--- /dev/null
+++ b/app/src/main/res/layout/history_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_book_contents.xml b/app/src/main/res/layout/search_book_contents.xml
new file mode 100644
index 0000000..f572a0d
--- /dev/null
+++ b/app/src/main/res/layout/search_book_contents.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/search_book_contents_header.xml b/app/src/main/res/layout/search_book_contents_header.xml
new file mode 100644
index 0000000..09d627e
--- /dev/null
+++ b/app/src/main/res/layout/search_book_contents_header.xml
@@ -0,0 +1,24 @@
+
+
+
diff --git a/app/src/main/res/layout/search_book_contents_list_item.xml b/app/src/main/res/layout/search_book_contents_list_item.xml
new file mode 100644
index 0000000..0238154
--- /dev/null
+++ b/app/src/main/res/layout/search_book_contents_list_item.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/share.xml b/app/src/main/res/layout/share.xml
new file mode 100644
index 0000000..785cd0f
--- /dev/null
+++ b/app/src/main/res/layout/share.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/capture.xml b/app/src/main/res/menu/capture.xml
new file mode 100644
index 0000000..fef1ad4
--- /dev/null
+++ b/app/src/main/res/menu/capture.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/encode.xml b/app/src/main/res/menu/encode.xml
new file mode 100644
index 0000000..b6b1237
--- /dev/null
+++ b/app/src/main/res/menu/encode.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/history.xml b/app/src/main/res/menu/history.xml
new file mode 100644
index 0000000..17faa05
--- /dev/null
+++ b/app/src/main/res/menu/history.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/raw/beep.ogg b/app/src/main/res/raw/beep.ogg
new file mode 100644
index 0000000..1419947
Binary files /dev/null and b/app/src/main/res/raw/beep.ogg differ
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..08959d5
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,62 @@
+
+
+
+
+ - -
+ - AR
+ - AU
+ - BR
+ - BG
+ - CA
+ - CH
+ - CN
+ - CZ
+ - DE
+ - DK
+ - ES
+ - FI
+ - FR
+ - GB
+ - GR
+ - HU
+ - ID
+ - IT
+ - JP
+ - KR
+ - NL
+ - PL
+ - PT
+ - RO
+ - RU
+ - SE
+ - SK
+ - SI
+ - TR
+ - TW
+ - US
+
+
+ - ON
+ - AUTO
+ - OFF
+
+
+ - @string/preferences_front_light_on
+ - @string/preferences_front_light_auto
+ - @string/preferences_front_light_off
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3ab3e9c..90633b0 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,15 @@
#3F51B5
#303F9F
#FF4081
+ #ff000000
+ #ffffffff
+ #c0ffbd21
+ #ffc0c0c0
+ #c099cc00
+ #ffffffff
+ #b0000000
+ #ffffffff
+ #00000000
+ #ffcc0000
+ #60000000
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e742b28..869e78e 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -4,4 +4,9 @@
75dp
200dp
16dp
+
+ 16dp
+ 16dp
+ 8dip
+ 4dip
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..58399b9
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9684ed6..cb93174 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,129 @@
PillDroid
Medicament Detail
+ Applications
+ Bookmarks
+ Add to calendar
+ Add contact
+ Book Search
+ Cancel
+ Custom search
+ Dial number
+ Send email
+ Get directions
+ Send MMS
+ OK
+ Open browser
+ Product search
+ Search contents
+ Application
+ Bookmark
+ Share via email
+ Share via SMS
+ Clipboard
+ Contact
+ Show map
+ Send SMS
+ Web search
+ Connect to Network
+ Contact info
+ Email address
+ Geographic coordinates
+ Phone number
+ SMS address
+ Plain text
+ Clear history
+ Clear
+ Barcode Scanner history
+ Empty
+ No barcode scans have been recorded
+ Send history
+ History
+ Use MECARD
+ Use vCard
+ Help
+ History
+ Settings
+ Share
+ Bulk mode: barcode scanned and saved
+ Sorry, the Android camera encountered a problem. You may need to restart the device.
+ Format
+ Metadata
+ Hi
+ Place a barcode inside the viewfinder rectangle to scan it.
+ Time
+ Type
+ Could not encode a barcode from the data provided.
+ Error
+ Google
+ Google
+ Sorry, the requested application could not be launched. The barcode contents may be invalid.
+ Invalid value
+ Redirect
+ Sorry, this book is not searchable.
+ Sorry, the search encountered a problem.
+ No page returned
+ Page
+ Results
+ Searching book\u2026
+ Snippet not available
+ You can share data by displaying a barcode on your screen and scanning it with another phone.
+ Or type some text and press Enter
+ Are you sure?
+ Sorry, the SD card is not accessible.
+ When a barcode is found\u2026
+ Use auto focus
+ Open web pages automatically
+ Scan and save many barcodes continuously
+ Bulk scan mode
+ Copy to clipboard
+ Substitutions: %s = contents, %f = format, %t = type
+ Custom search URL
+ 1D Industrial
+ 1D Product
+ Aztec
+ Data Matrix
+ PDF417 (β)
+ QR Codes
+ Device Bug Workarounds
+ No barcode scene mode
+ Use only standard focus mode
+ No continuous focus
+ No exposure
+ No metering
+ Improves scanning in low light on some phones, but may cause glare. Does not work on all phones.
+ Use front light
+ Automatic
+ Off
+ On
+ General settings
+ Store your scans in History
+ Add to History
+ Invert scan
+ Scan for white barcodes on black background. Not available on some devices.
+ Settings
+ No automatic rotation
+ Beep
+ Store multiple scans of the same barcode in History
+ Remember duplicates
+ Result settings
+ When scanning for barcodes, decode\u2026
+ Search country
+ Try to retrieve more information about the barcode contents
+ Retrieve more info
+ Vibrate
+ Found contact info
+ Found calendar event
+ Found email address
+ Found geographic coordinates
+ Found book
+ Found product
+ Found SMS address
+ Found phone number
+ Found plain text
+ Found URL
+ Found WLAN Configuration
+ Google Book Search
+ Requesting connection to network\u2026
+ Médicament introuvable dans la base de données
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 545b9c6..8a9ee49 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -16,5 +16,13 @@
-
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..196fed1
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+