diff --git a/glass/.gitignore b/glass/.gitignore new file mode 100644 index 000000000..8213249e8 --- /dev/null +++ b/glass/.gitignore @@ -0,0 +1,6 @@ +target/ +*.iml +bin/ +gen/ +libs/ +local.properties diff --git a/glass/AndroidManifest.xml b/glass/AndroidManifest.xml new file mode 100644 index 000000000..2809c1960 --- /dev/null +++ b/glass/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/glass/pom.xml b/glass/pom.xml new file mode 100644 index 000000000..a33e70258 --- /dev/null +++ b/glass/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + glass + 0.2.0 + apk + + + + com.google.zxing + core + ${project.parent.version} + + + com.google.android + android + + + + + com.google.zxing + zxing-parent + 3.0.2-SNAPSHOT + + + + src + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + + + + + Android Barcode Scanner app for Google Glass + Provides the Google Glass app "Barcode Scanner" + + diff --git a/glass/proguard.cfg b/glass/proguard.cfg new file mode 100644 index 000000000..3dc25f17f --- /dev/null +++ b/glass/proguard.cfg @@ -0,0 +1,68 @@ +# This is a configuration file for ProGuard. +# http://proguard.sourceforge.net/index.html#manual/usage.html + +# Optimizations: If you don't want to optimize, use the +# proguard-android.txt configuration file instead of this one, which +# turns off the optimization flags. Adding optimization introduces +# certain risks, since for example not all optimizations performed by +# ProGuard works on all versions of Dalvik. The following flags turn +# off various optimizations known to have issues, but the list may not +# be complete or up to date. (The "arithmetic" optimization can be +# used if you are only targeting Android 2.0 or later.) Make sure you +# test thoroughly if you go this route. +-optimizations !code/simplification/cast,!field/*,!class/merging/*,!code/allocation/variable,!method/marking/private +-optimizationpasses 5 +-allowaccessmodification +-dontpreverify + +# The remainder of this file is identical to the non-optimized version +# of the Proguard configuration file (except that the other file has +# flags to turn off optimization). + +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-verbose + +# ADDED +-dontshrink +-dontobfuscate + +-keepattributes *Annotation* +-keep public class com.google.vending.licensing.ILicensingService +-keep public class com.android.vending.licensing.ILicensingService + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} + +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} + +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keepclassmembers class **.R$* { + public static ; +} + +# The support library contains references to newer platform versions. +# Don't warn about those in case this app is linking against an older +# platform version. We know about them, and they are safe. +-dontwarn android.support.** diff --git a/glass/res/drawable-hdpi/ic_launcher.png b/glass/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..5932aadea Binary files /dev/null and b/glass/res/drawable-hdpi/ic_launcher.png differ diff --git a/glass/res/layout/capture.xml b/glass/res/layout/capture.xml new file mode 100644 index 000000000..9b86a20d9 --- /dev/null +++ b/glass/res/layout/capture.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/glass/res/values/colors.xml b/glass/res/values/colors.xml new file mode 100755 index 000000000..c1e8ff2ba --- /dev/null +++ b/glass/res/values/colors.xml @@ -0,0 +1,20 @@ + + + + #ffffffff + #b0000000 + diff --git a/glass/res/values/ids.xml b/glass/res/values/ids.xml new file mode 100755 index 000000000..c640cab2c --- /dev/null +++ b/glass/res/values/ids.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/glass/res/values/strings.xml b/glass/res/values/strings.xml new file mode 100644 index 000000000..cffbf3f68 --- /dev/null +++ b/glass/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + Barcode Scanner for Glass + diff --git a/glass/res/xml/barcode_scanner_show.xml b/glass/res/xml/barcode_scanner_show.xml new file mode 100644 index 000000000..94ca46b93 --- /dev/null +++ b/glass/res/xml/barcode_scanner_show.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/glass/src/com/google/zxing/client/glass/CameraConfigurationManager.java b/glass/src/com/google/zxing/client/glass/CameraConfigurationManager.java new file mode 100644 index 000000000..d343946c0 --- /dev/null +++ b/glass/src/com/google/zxing/client/glass/CameraConfigurationManager.java @@ -0,0 +1,128 @@ +/* + * 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.glass; + +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.Log; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Sean Owen + */ +final class CameraConfigurationManager { + + private static final String TAG = "CameraConfiguration"; + private static final int AREA_PER_1000 = 400; + private static final int MIN_FPS = 10; + + private CameraConfigurationManager() { + } + + static void configure(Camera camera) { + Camera.Parameters parameters = camera.getParameters(); + //parameters.setPreviewSize(1024, 768); + parameters.setPreviewSize(512, 288); + //configureAdvanced(parameters); + camera.setParameters(parameters); + } + + private static void configureAdvanced(Camera.Parameters parameters) { + + setBestPreviewFPS(parameters); + + String sceneMode = findSettableValue(parameters.getSupportedSceneModes(), + Camera.Parameters.SCENE_MODE_BARCODE); + if (sceneMode != null) { + parameters.setSceneMode(sceneMode); + } else { + Log.i(TAG, "Scene mode is not supported"); + } + + if (parameters.isVideoStabilizationSupported()) { + Log.i(TAG, "Enabling video stabilization..."); + parameters.setVideoStabilization(true); + } else { + Log.i(TAG, "This device does not support video stabilization"); + } + + if (parameters.getMaxNumMeteringAreas() > 0) { + Log.i(TAG, "Old metering areas: " + parameters.getMeteringAreas()); + List middleArea = Collections.singletonList( + new Camera.Area(new Rect(-AREA_PER_1000, -AREA_PER_1000, AREA_PER_1000, AREA_PER_1000), 1)); + parameters.setMeteringAreas(middleArea); + } else { + Log.i(TAG, "Device does not support metering areas"); + } + + if (parameters.isZoomSupported()) { + Log.i(TAG, "Setting to max zoom"); + parameters.setZoom(parameters.getMaxZoom()); + } else { + Log.i(TAG, "Zoom is not supported"); + } + + } + + private static String findSettableValue(Collection supportedValues, + String... desiredValues) { + Log.i(TAG, "Supported values: " + supportedValues); + String result = null; + if (supportedValues != null) { + for (String desiredValue : desiredValues) { + if (supportedValues.contains(desiredValue)) { + result = desiredValue; + break; + } + } + } + Log.i(TAG, "Settable value: " + result); + return result; + } + + private static void setBestPreviewFPS(Camera.Parameters parameters) { + // Required for Glass compatibility; also improves battery/CPU performance a tad + List supportedPreviewFpsRanges = parameters.getSupportedPreviewFpsRange(); + if (supportedPreviewFpsRanges != null && !supportedPreviewFpsRanges.isEmpty()) { + int[] minimumSuitableFpsRange = null; + for (int[] fpsRange : supportedPreviewFpsRanges) { + int fpsMax = fpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; + if (fpsMax >= MIN_FPS * 1000 && + (minimumSuitableFpsRange == null || + fpsMax > minimumSuitableFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX])) { + minimumSuitableFpsRange = fpsRange; + } + } + if (minimumSuitableFpsRange == null) { + Log.i(TAG, "No suitable FPS range?"); + } else { + int[] currentFpsRange = new int[2]; + parameters.getPreviewFpsRange(currentFpsRange); + if (!Arrays.equals(currentFpsRange, minimumSuitableFpsRange)) { + Log.i(TAG, "Setting FPS range to " + Arrays.toString(minimumSuitableFpsRange)); + parameters.setPreviewFpsRange(minimumSuitableFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + minimumSuitableFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + } + } + } + } + +} diff --git a/glass/src/com/google/zxing/client/glass/CaptureActivity.java b/glass/src/com/google/zxing/client/glass/CaptureActivity.java new file mode 100644 index 000000000..466f3537d --- /dev/null +++ b/glass/src/com/google/zxing/client/glass/CaptureActivity.java @@ -0,0 +1,186 @@ +/* + * 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.glass; + +import android.app.Activity; +import android.content.Intent; +import android.hardware.Camera; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.TextView; +import com.google.zxing.Result; +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ParsedResultType; +import com.google.zxing.client.result.ResultParser; +import com.google.zxing.client.result.TextParsedResult; +import com.google.zxing.client.result.URIParsedResult; + +import java.io.IOException; + +/** + * @author Sean Owen + */ +public final class CaptureActivity extends Activity implements SurfaceHolder.Callback { + + private static final String TAG = CaptureActivity.class.getSimpleName(); + + private boolean hasSurface; + private SurfaceHolder holderWithCallback; + private Camera camera; + private DecodeRunnable decodeRunnable; + private Result result; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setContentView(R.layout.capture); + } + + @Override + public synchronized void onResume() { + super.onResume(); + SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view); + SurfaceHolder surfaceHolder = surfaceView.getHolder(); + if (surfaceHolder == null) { + throw new IllegalStateException("No SurfaceHolder?"); + } + if (hasSurface) { + initCamera(surfaceHolder); + } else { + surfaceHolder.addCallback(this); + holderWithCallback = surfaceHolder; + } + } + + @Override + public synchronized void onPause() { + result = null; + if (decodeRunnable != null) { + decodeRunnable.stop(); + decodeRunnable = null; + } + if (camera != null) { + camera.stopPreview(); + camera.release(); + camera = null; + } + if (holderWithCallback != null) { + holderWithCallback.removeCallback(this); + holderWithCallback = null; + } + super.onPause(); + } + + @Override + public synchronized void surfaceCreated(SurfaceHolder holder) { + Log.i(TAG, "Surface created"); + holderWithCallback = null; + if (!hasSurface) { + hasSurface = true; + initCamera(holder); + } + } + + @Override + public synchronized void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // do nothing + } + + @Override + public synchronized void surfaceDestroyed(SurfaceHolder holder) { + Log.i(TAG, "Surface destroyed"); + holderWithCallback = null; + hasSurface = false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (result != null) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + handleResult(result); + return true; + case KeyEvent.KEYCODE_BACK: + reset(); + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + private void initCamera(SurfaceHolder holder) { + if (camera != null) { + throw new IllegalStateException("Camera not null on initialization"); + } + camera = Camera.open(); + if (camera == null) { + throw new IllegalStateException("Camera is null"); + } + + CameraConfigurationManager.configure(camera); + + try { + camera.setPreviewDisplay(holder); + camera.startPreview(); + } catch (IOException e) { + Log.e(TAG, "Cannot start prevew", e); + } + + decodeRunnable = new DecodeRunnable(this, camera); + new Thread(decodeRunnable).start(); + reset(); + } + + void setResult(Result result) { + TextView statusView = (TextView) findViewById(R.id.status_view); + String text = result.getText(); + statusView.setText(text); + statusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.max(14, 72 - text.length() / 2)); + statusView.setVisibility(View.VISIBLE); + this.result = result; + } + + private void handleResult(Result result) { + ParsedResult parsed = ResultParser.parseResult(result); + Intent intent; + if (parsed.getType() == ParsedResultType.URI) { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(((URIParsedResult) parsed).getURI())); + } else { + intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra("query", ((TextParsedResult) parsed).getText()); + } + startActivity(intent); + } + + private void reset() { + TextView statusView = (TextView) findViewById(R.id.status_view); + statusView.setVisibility(View.GONE); + result = null; + decodeRunnable.startScanning(); + } + +} diff --git a/glass/src/com/google/zxing/client/glass/DecodeRunnable.java b/glass/src/com/google/zxing/client/glass/DecodeRunnable.java new file mode 100644 index 000000000..4f94225ad --- /dev/null +++ b/glass/src/com/google/zxing/client/glass/DecodeRunnable.java @@ -0,0 +1,160 @@ +/* + * 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.glass; + +import android.hardware.Camera; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +/** + * @author Sean Owen + */ +final class DecodeRunnable implements Runnable, Camera.PreviewCallback { + + private static final String TAG = DecodeRunnable.class.getSimpleName(); + + private final CaptureActivity activity; + private final Camera camera; + private final int height; + private final int width; + private boolean running; + private Handler handler; + private final CountDownLatch handlerInitLatch; + + DecodeRunnable(CaptureActivity activity, Camera camera) { + this.activity = activity; + this.camera = camera; + Camera.Parameters parameters = camera.getParameters(); + Camera.Size previewSize = parameters.getPreviewSize(); + height = previewSize.height; + width = previewSize.width; + running = true; + handlerInitLatch = new CountDownLatch(1); + } + + private Handler getHandler() { + try { + handlerInitLatch.await(); + } catch (InterruptedException ie) { + // continue? + } + return handler; + } + + + @Override + public void run() { + Looper.prepare(); + handler = new DecodeHandler(); + handlerInitLatch.countDown(); + Looper.loop(); + } + + void startScanning() { + getHandler().obtainMessage(R.id.decode_failed).sendToTarget(); + } + + void stop() { + getHandler().obtainMessage(R.id.quit).sendToTarget(); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if (running) { + getHandler().obtainMessage(R.id.decode, data).sendToTarget(); + } + } + + private final class DecodeHandler extends Handler { + + private final Map hints; + + DecodeHandler() { + hints = new EnumMap<>(DecodeHintType.class); + hints.put(DecodeHintType.POSSIBLE_FORMATS, + Arrays.asList(BarcodeFormat.AZTEC, BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX)); + } + + @Override + public void handleMessage(Message message) { + if (!running) { + return; + } + switch (message.what) { + case R.id.decode: + decode((byte[]) message.obj); + break; + case R.id.decode_succeeded: + final Result result = (Result) message.obj; + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + activity.setResult(result); + } + }); + break; + case R.id.decode_failed: + camera.setOneShotPreviewCallback(DecodeRunnable.this); + break; + case R.id.quit: + running = false; + Looper.myLooper().quit(); + break; + } + } + + private void decode(byte[] data) { + Result rawResult = null; + PlanarYUVLuminanceSource source = + new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + try { + rawResult = new MultiFormatReader().decode(bitmap, hints); + } catch (ReaderException re) { + // continue + } + + Handler handler = getHandler(); + Message message; + if (rawResult == null) { + message = handler.obtainMessage(R.id.decode_failed); + } else { + Log.i(TAG, "Decode succeeded: " + rawResult.getText()); + message = handler.obtainMessage(R.id.decode_succeeded, rawResult); + } + message.sendToTarget(); + } + + } + +} diff --git a/pom.xml b/pom.xml index 956706303..6d8bbd065 100644 --- a/pom.xml +++ b/pom.xml @@ -509,6 +509,7 @@ android androidtest + glass