diff --git a/CHANGES b/CHANGES
index 7e4661a2b..9de2209e6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -319,10 +319,11 @@
- Many ports of Java updates to C++ and C# ports
- Standardize test system image processing and image format for reproducibility
-2.3 (XX Dec 2013)
+2.3.0 (1 Dec 2013)
- Added clone of Google Chart Server QR code encoder API to zxingorg/, at endpoint /chart
- Updated zxing.appspot.com generator to remove deprecated API usage, use latest GWT / App Engine
+ - Added skeleton Google Glass Mirror API app
- Improve and standardize Maven build, standardize directories and version naming
- Barcode Scanner Android up requires Android 4 and is updated to use Android 4+ APIs
- javase/ and zxingorg/ modules now use Java 7
diff --git a/build.xml b/build.xml
index 25cbefc08..5c0efc594 100644
--- a/build.xml
+++ b/build.xml
@@ -43,6 +43,7 @@
+
diff --git a/glass-mirror/pom.xml b/glass-mirror/pom.xml
new file mode 100755
index 000000000..7de963daa
--- /dev/null
+++ b/glass-mirror/pom.xml
@@ -0,0 +1,145 @@
+
+
+
+
+ 4.0.0
+
+ glass-mirror
+ 2.3.0-SNAPSHOT
+ war
+
+
+
+
+ com.google.apis
+ google-api-services-mirror
+ v1-rev28-1.17.0-rc
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ com.google.http-client
+ google-http-client-jackson2
+ 1.17.0-rc
+
+
+ com.google.apis
+ google-api-services-oauth2
+ v2-rev51-1.17.0-rc
+
+
+
+ com.google.zxing
+ core
+ ${project.version}
+
+
+ com.google.zxing
+ javase
+ ${project.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.2.3
+
+
+ commons-codec
+ commons-codec
+ 1.8
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.3.1
+ runtime
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.apache.httpcomponents
+ httpcore
+ 4.3
+
+
+
+ com.google.guava
+ guava
+ 15.0
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.5
+
+
+ org.slf4j
+ slf4j-jdk14
+ 1.7.5
+ runtime
+
+
+ org.slf4j
+ jcl-over-slf4j
+ 1.7.5
+ runtime
+
+
+ javax
+ javaee-web-api
+ 6.0
+ provided
+
+
+
+
+
+ com.google.zxing
+ zxing-parent
+ 2.3.0-SNAPSHOT
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 7
+
+
+
+
+
+ ZXing Google Glass Mirror API app
+ Experimental Barcode Scanner app for Google Glass Mirror API
+
+
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/AuthUtil.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/AuthUtil.java
new file mode 100755
index 000000000..4693c2a05
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/AuthUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ *
+ * 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.
+ */
+
+/*
+ * Copyright 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.glass;
+
+import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.util.store.DataStoreFactory;
+import com.google.api.client.util.store.MemoryDataStoreFactory;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * A collection of utility functions that simplify common authentication and
+ * user identity tasks
+ *
+ * @author Sean Owen
+ * @author Google
+ */
+final class AuthUtil {
+
+ private static final Collection SCOPES =
+ Arrays.asList("https://www.googleapis.com/auth/glass.timeline",
+ "https://www.googleapis.com/auth/userinfo.profile");
+
+ private final DataStoreFactory dataStoreFactory;
+ private final String clientID;
+ private final String clientSecret;
+
+ AuthUtil(String clientID, String clientSecret) {
+ dataStoreFactory = new MemoryDataStoreFactory();
+ this.clientID = clientID;
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Creates and returns a new {@link AuthorizationCodeFlow} for this app.
+ */
+ public AuthorizationCodeFlow newAuthorizationCodeFlow() throws IOException {
+ return new GoogleAuthorizationCodeFlow.Builder(new NetHttpTransport(),
+ new JacksonFactory(),
+ clientID,
+ clientSecret,
+ SCOPES)
+ .setAccessType("offline").setDataStoreFactory(dataStoreFactory).build();
+ }
+
+ public Credential getCredential(String userId) throws IOException {
+ return userId == null ? null : newAuthorizationCodeFlow().loadCredential(userId);
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/DecodeHelper.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/DecodeHelper.java
new file mode 100755
index 000000000..dad9d24da
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/DecodeHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 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.glass;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.Reader;
+import com.google.zxing.ReaderException;
+import com.google.zxing.Result;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.GlobalHistogramBinarizer;
+import com.google.zxing.common.HybridBinarizer;
+
+import java.awt.color.CMMException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
+/**
+ * @author Sean Owen
+ */
+final class DecodeHelper {
+
+ // No real reason to deal with more than maybe 8.3 megapixels
+ private static final int MAX_PIXELS = 1 << 23;
+ private static final Map HINTS;
+
+ static {
+ HINTS = new EnumMap<>(DecodeHintType.class);
+ HINTS.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
+ HINTS.put(DecodeHintType.POSSIBLE_FORMATS, EnumSet.allOf(BarcodeFormat.class));
+ }
+
+ private DecodeHelper() {
+ }
+
+ static Collection processStream(InputStream is) throws IOException {
+ BufferedImage image;
+ try {
+ image = ImageIO.read(is);
+ } catch (CMMException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ if (image == null) {
+ throw new IOException("No image");
+ }
+ if (image.getHeight() <= 1 || image.getWidth() <= 1 ||
+ image.getHeight() * image.getWidth() > MAX_PIXELS) {
+ throw new IOException("Dimensions out of bounds: " + image.getWidth() + 'x' + image.getHeight());
+ }
+
+ return processImage(image);
+ }
+
+ private static Collection processImage(BufferedImage image) {
+
+ Reader reader = new MultiFormatReader();
+ LuminanceSource source = new BufferedImageLuminanceSource(image);
+ BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
+ Collection results = new ArrayList<>(1);
+
+ if (results.isEmpty()) {
+ try {
+ // Look for normal barcode in photo
+ Result theResult = reader.decode(bitmap, HINTS);
+ if (theResult != null) {
+ results.add(theResult);
+ }
+ } catch (ReaderException re) {
+ // continue
+ }
+ }
+
+ if (results.isEmpty()) {
+ try {
+ // Try again with other binarizer
+ BinaryBitmap hybridBitmap = new BinaryBitmap(new HybridBinarizer(source));
+ Result theResult = reader.decode(hybridBitmap, HINTS);
+ if (theResult != null) {
+ results.add(theResult);
+ }
+ } catch (ReaderException re) {
+ // continue
+ }
+ }
+
+ return results;
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/NewUserSetupServlet.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/NewUserSetupServlet.java
new file mode 100755
index 000000000..558d84425
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/NewUserSetupServlet.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ *
+ * 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.
+ */
+
+/*
+ * Copyright 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.glass;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.ResourceBundle;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.services.mirror.model.Contact;
+import com.google.api.services.mirror.model.Subscription;
+import com.google.api.services.mirror.model.TimelineItem;
+import com.google.zxing.client.glass.mirror.MirrorClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This servlet manages the OAuth 2.0 dance.
+ *
+ * @author Jenny Murphy - http://google.com/+JennyMurphy
+ * @author Sean Owen
+ * @link https://developers.google.com/glass/develop/mirror/authorization
+ */
+public final class NewUserSetupServlet extends HttpServlet {
+
+ private static final Logger log = LoggerFactory.getLogger(NewUserSetupServlet.class);
+
+ private static final String BS_ID = "barcode-scanner";
+
+ private AuthUtil authUtil;
+ private String baseURL;
+ private String notificationCallbackURL;
+ private String subscriptionVerifyToken;
+
+ @Override
+ public void init(ServletConfig servletConfig) {
+ ServletContext context = servletConfig.getServletContext();
+ authUtil = new AuthUtil(context.getInitParameter("CLIENT_ID"), context.getInitParameter("CLIENT_SECRET"));
+ baseURL = context.getInitParameter("BASE_URL");
+ notificationCallbackURL = baseURL + "/notification";
+ subscriptionVerifyToken = context.getInitParameter("SUBSCRIPTION_VERIFY_TOKEN");
+ }
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+ String redirect;
+
+ if (request.getParameter("error") != null) {
+
+ log.warn("OAuth error: {}", Arrays.toString(request.getParameterValues("error")));
+ redirect = baseURL;
+
+ } else {
+
+ GenericUrl url1 = new GenericUrl(request.getRequestURL().toString());
+ url1.setRawPath("/oauth2callback");
+ String callbackURL = url1.build();
+ String code = request.getParameter("code");
+
+ if (code == null) {
+
+ log.info("New OAuth flow");
+
+ AuthorizationCodeFlow flow = authUtil.newAuthorizationCodeFlow();
+ GenericUrl url = flow.newAuthorizationUrl().setRedirectUri(callbackURL);
+ url.set("approval_prompt", "force");
+ redirect = url.build();
+
+ } else {
+ // If we have a code, finish the OAuth 2.0 dance
+
+ log.info("OAuth response, code: {}", code);
+
+ AuthorizationCodeFlow flow = authUtil.newAuthorizationCodeFlow();
+ TokenResponse tokenResponse = flow.newTokenRequest(code).setRedirectUri(callbackURL).execute();
+
+ // Extract the Google User ID from the ID token in the auth response
+ String userId = ((GoogleTokenResponse) tokenResponse).parseIdToken().getPayload().getSubject();
+
+ // Set it into the session
+ //AuthUtil.setUserId(request, userId);
+ flow.createAndStoreCredential(tokenResponse, userId);
+
+ ResourceBundle resources = ResourceBundle.getBundle("Strings", request.getLocale());
+
+ bootstrapNewUser(userId, resources);
+
+ redirect = "glass/index.jspx";
+ }
+ }
+
+ response.sendRedirect(redirect);
+ }
+
+ private void bootstrapNewUser(String userId, ResourceBundle resources) throws IOException {
+ Credential credential = authUtil.newAuthorizationCodeFlow().loadCredential(userId);
+
+ String appName = resources.getString("app.title");
+
+ MirrorClient mirrorClient = MirrorClient.get();
+
+ boolean contactExists = false;
+ for (Contact contact : mirrorClient.listContacts(credential)) {
+ if (BS_ID.equals(contact.getId())) {
+ contactExists = true;
+ break;
+ }
+ }
+
+ if (!contactExists) {
+ Contact contact = new Contact();
+ contact.setId(BS_ID);
+ contact.setDisplayName(appName);
+ contact.setAcceptTypes(Arrays.asList("image/png", "image/jpeg", "image/gif", "image/bmp"));
+ mirrorClient.insertContact(credential, contact);
+ }
+
+ boolean subscriptionExists = false;
+ for (Subscription subscription : mirrorClient.listSubscriptions(credential)) {
+ if (notificationCallbackURL.equals(subscription.getCallbackUrl())) {
+ subscriptionExists = true;
+ break;
+ }
+ }
+
+ if (!subscriptionExists) {
+ Subscription subscription = new Subscription();
+ subscription.setCollection("timeline");
+ subscription.setOperation(Collections.singletonList("INSERT"));
+ subscription.setCallbackUrl(notificationCallbackURL);
+ subscription.setVerifyToken(subscriptionVerifyToken);
+ subscription.setUserToken(userId);
+ mirrorClient.insertSubscription(credential, subscription);
+ }
+
+ TimelineItem timelineItem = new TimelineItem();
+ timelineItem.setTitle(MessageFormat.format(resources.getString("setup.welcome.title"), appName));
+ timelineItem.setText(MessageFormat.format(resources.getString("setup.welcome.text"), appName));
+ mirrorClient.insertTimelineItem(credential, timelineItem);
+
+ }
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationCallable.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationCallable.java
new file mode 100755
index 000000000..ead95e079
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationCallable.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.glass;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.concurrent.Callable;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.services.mirror.model.Attachment;
+import com.google.api.services.mirror.model.TimelineItem;
+import com.google.zxing.Result;
+import com.google.zxing.client.result.ParsedResult;
+import com.google.zxing.client.result.ResultParser;
+import com.google.zxing.client.result.URIParsedResult;
+import com.google.zxing.client.glass.mirror.MirrorClient;
+
+/**
+ * @author Sean Owen
+ */
+final class NotificationCallable implements Callable {
+
+ private final Credential credential;
+ private final String timelineItemID;
+ private final ResourceBundle resources;
+
+ NotificationCallable(Credential credential, String timelineItemID, ResourceBundle resources) {
+ this.credential = credential;
+ this.timelineItemID = timelineItemID;
+ this.resources = resources;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ MirrorClient mirrorClient = MirrorClient.get();
+ TimelineItem timelineItem = mirrorClient.getTimelineItem(credential, timelineItemID);
+ if (timelineItem == null) {
+ return null;
+ }
+ List attachments = timelineItem.getAttachments();
+ if (attachments == null || attachments.isEmpty()) {
+ return null;
+ }
+ Attachment attachment = attachments.get(0);
+ String attachmentId = attachment.getId();
+
+ String errorMessage = null;
+ Collection results = null;
+ try (InputStream in = mirrorClient.getAttachmentInputStream(credential, timelineItemID, attachmentId)) {
+ results = DecodeHelper.processStream(in);
+ } catch (IOException | RuntimeException e) {
+ errorMessage = e.toString();
+ }
+
+ if (results == null || results.isEmpty()) {
+
+ TimelineItem resultTimelineItem = new TimelineItem();
+ if (errorMessage == null) {
+ resultTimelineItem.setTitle(resources.getString("scan.result.notfound.title"));
+ resultTimelineItem.setText(resources.getString("scan.result.notfound.text"));
+ } else {
+ resultTimelineItem.setTitle(resources.getString("scan.result.error.title"));
+ resultTimelineItem.setText(MessageFormat.format(resources.getString("scan.result.error.text"), errorMessage));
+ }
+ mirrorClient.insertTimelineItem(credential, resultTimelineItem);
+
+ } else {
+
+ for (Result result : results) {
+ ParsedResult parsedResult = ResultParser.parseResult(result);
+
+ TimelineItem resultTimelineItem = new TimelineItem();
+ resultTimelineItem.setText(parsedResult.getDisplayResult());
+ resultTimelineItem.setTitle(
+ MessageFormat.format(resources.getString("scan.result.success.title"), parsedResult.getType()));
+
+ addResultDetailToTimelineItem(parsedResult, resultTimelineItem);
+
+ mirrorClient.insertTimelineItem(credential, resultTimelineItem);
+ }
+
+ }
+
+ return null;
+ }
+
+ private static void addResultDetailToTimelineItem(ParsedResult parsedResult, TimelineItem item) {
+ if (parsedResult instanceof URIParsedResult) {
+ addURIResultDetailToTimelineItem((URIParsedResult) parsedResult, item);
+ }
+ }
+
+ private static void addURIResultDetailToTimelineItem(URIParsedResult parsedResult, TimelineItem item) {
+ String uri = parsedResult.getURI();
+ item.setHtml("" + parsedResult.getDisplayResult() + "");
+ item.setCanonicalUrl(uri);
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationServlet.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationServlet.java
new file mode 100755
index 000000000..5fb200ea7
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/NotificationServlet.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 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.glass;
+
+import java.io.IOException;
+import java.util.ResourceBundle;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.mirror.model.Notification;
+
+/**
+ * @author Sean Owen
+ * @link https://developers.google.com/glass/develop/mirror/timeline
+ */
+public final class NotificationServlet extends HttpServlet {
+
+ private ExecutorService executor;
+ private final JsonObjectParser jsonParser = new JsonObjectParser(new JacksonFactory());
+ private AuthUtil authUtil;
+ private String subscriptionVerifyToken;
+
+ @Override
+ public void init(ServletConfig servletConfig) {
+ ServletContext context = servletConfig.getServletContext();
+ authUtil = new AuthUtil(context.getInitParameter("CLIENT_ID"), context.getInitParameter("CLIENT_SECRET"));
+ executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+ subscriptionVerifyToken = context.getInitParameter("SUBSCRIPTION_VERIFY_TOKEN");
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+ Notification notification = jsonParser.parseAndClose(request.getReader(), Notification.class);
+
+ if (!subscriptionVerifyToken.equals(notification.getVerifyToken())) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ String userId = notification.getUserToken();
+ Credential credential = authUtil.newAuthorizationCodeFlow().loadCredential(userId);
+ String timelineItemID = notification.getItemId();
+
+ ResourceBundle resources = ResourceBundle.getBundle("Strings", request.getLocale());
+
+ executor.submit(new NotificationCallable(credential, timelineItemID, resources));
+ }
+
+ @Override
+ public void destroy() {
+ executor.shutdownNow();
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/GoogleAPIMirrorClient.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/GoogleAPIMirrorClient.java
new file mode 100755
index 000000000..f12b6a582
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/GoogleAPIMirrorClient.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ *
+ * 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.
+ */
+
+/*
+ * Copyright 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.glass.mirror;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.mirror.Mirror;
+import com.google.api.services.mirror.model.Attachment;
+import com.google.api.services.mirror.model.Contact;
+import com.google.api.services.mirror.model.Subscription;
+import com.google.api.services.mirror.model.TimelineItem;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+final class GoogleAPIMirrorClient extends MirrorClient {
+
+ @Override
+ public void insertContact(Credential credential, Contact contact) throws IOException {
+ getMirror(credential).contacts().insert(contact).execute();
+ }
+
+ @Override
+ public Collection listContacts(Credential credential) throws IOException {
+ return getMirror(credential).contacts().list().execute().getItems();
+ }
+
+ @Override
+ public void insertTimelineItem(Credential credential, TimelineItem item) throws IOException {
+ getMirror(credential).timeline().insert(item).execute();
+ }
+
+ @Override
+ public void insertSubscription(Credential credential, Subscription subscription) throws IOException {
+ getMirror(credential).subscriptions().insert(subscription).execute();
+ }
+
+ @Override
+ public Collection listSubscriptions(Credential credential) throws IOException {
+ return getMirror(credential).subscriptions().list().execute().getItems();
+ }
+
+ @Override
+ public TimelineItem getTimelineItem(Credential credential, String id) throws IOException {
+ return getMirror(credential).timeline().get(id).execute();
+ }
+
+ @Override
+ public InputStream getAttachmentInputStream(Credential credential, String timelineItemId, String attachmentId)
+ throws IOException {
+ Mirror mirrorService = getMirror(credential);
+ Mirror.Timeline.Attachments attachments = mirrorService.timeline().attachments();
+ Attachment attachmentMetadata = attachments.get(timelineItemId, attachmentId).execute();
+ HttpResponse resp =
+ mirrorService.getRequestFactory()
+ .buildGetRequest(new GenericUrl(attachmentMetadata.getContentUrl())).execute();
+ return resp.getContent();
+ }
+
+ private static Mirror getMirror(Credential credential) {
+ return new Mirror.Builder(new NetHttpTransport(), new JacksonFactory(), credential).build();
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MemoryMirrorClient.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MemoryMirrorClient.java
new file mode 100755
index 000000000..7ae382257
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MemoryMirrorClient.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 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.glass.mirror;
+
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.services.mirror.model.Attachment;
+import com.google.api.services.mirror.model.Contact;
+import com.google.api.services.mirror.model.Subscription;
+import com.google.api.services.mirror.model.TimelineItem;
+import com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class MemoryMirrorClient extends MirrorClient {
+
+ private static final Logger log = LoggerFactory.getLogger(MemoryMirrorClient.class);
+
+ private static final String FAKE_ATTACHMENT_ID = "0123";
+
+ private final Collection contacts =
+ Collections.synchronizedCollection(Sets.newHashSet());
+ private final Collection subscriptions =
+ Collections.synchronizedCollection(Sets.newHashSet());
+
+ @Override
+ public void insertContact(Credential credential, Contact contact) {
+ log.info("Insert Contact: {}", contact);
+ contacts.add(contact);
+ }
+
+ @Override
+ public Collection listContacts(Credential credential) {
+ log.info("Contacts: {}", contacts);
+ return contacts;
+ }
+
+ @Override
+ public void insertTimelineItem(Credential credential, TimelineItem item) {
+ log.info("Insert Timeline: {}", item);
+ }
+
+ @Override
+ public void insertSubscription(Credential credential, Subscription subscription) {
+ log.info("Insert Subscription: {}", subscription);
+ subscriptions.add(subscription);
+ }
+
+ @Override
+ public Collection listSubscriptions(Credential credential) {
+ log.info("Subscriptions: {}", subscriptions);
+ return subscriptions;
+ }
+
+ @Override
+ public TimelineItem getTimelineItem(Credential credential, String id) {
+ log.info("Get Timeline {}", id);
+ TimelineItem timelineItem = new TimelineItem();
+ timelineItem.setId(id);
+ Attachment fakeAttachment = new Attachment();
+ fakeAttachment.setId(FAKE_ATTACHMENT_ID);
+ timelineItem.setAttachments(Collections.singletonList(fakeAttachment));
+ return timelineItem;
+ }
+
+ @Override
+ public InputStream getAttachmentInputStream(Credential credential, String timelineItemId, String attachmentId) {
+ log.info("Get Attachment {} {}", timelineItemId, attachmentId);
+ if (!FAKE_ATTACHMENT_ID.equals(attachmentId)) {
+ return null;
+ }
+ return MemoryMirrorClient.class.getResourceAsStream("/test.jpg");
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MirrorClient.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MirrorClient.java
new file mode 100755
index 000000000..746b51c25
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/mirror/MirrorClient.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 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.glass.mirror;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.services.mirror.model.Contact;
+import com.google.api.services.mirror.model.Subscription;
+import com.google.api.services.mirror.model.TimelineItem;
+
+public abstract class MirrorClient {
+
+ private static MirrorClient instance = null;
+
+ public static synchronized MirrorClient get() {
+ if (instance == null) {
+ instance = new GoogleAPIMirrorClient();
+ }
+ return instance;
+ }
+
+ public abstract void insertContact(Credential credential, Contact contact) throws IOException;
+
+ public abstract Collection listContacts(Credential credential) throws IOException;
+
+ public abstract void insertTimelineItem(Credential credential, TimelineItem item) throws IOException;
+
+ public abstract void insertSubscription(Credential credential, Subscription subscription) throws IOException;
+
+ public abstract Collection listSubscriptions(Credential credential) throws IOException;
+
+ public abstract TimelineItem getTimelineItem(Credential credential, String id) throws IOException;
+
+ public abstract InputStream getAttachmentInputStream(Credential credential,
+ String timelineItemId,
+ String attachmentId) throws IOException;
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStore.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStore.java
new file mode 100644
index 000000000..b767734b7
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStore.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 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.glass.store;
+
+import com.google.api.client.auth.oauth2.StoredCredential;
+import com.google.api.client.util.store.AbstractDataStore;
+import com.google.api.client.util.store.DataStore;
+import com.google.api.client.util.store.DataStoreFactory;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+final class MySQLDataStore extends AbstractDataStore {
+
+ MySQLDataStore(DataStoreFactory dataStoreFactory, String id) {
+ super(dataStoreFactory, id);
+ }
+
+ @Override
+ public Set keySet() {
+ return Collections.singleton(StoredCredential.DEFAULT_DATA_STORE_ID);
+ }
+
+ @Override
+ public Collection values() throws IOException {
+ return null; // TODO
+ }
+
+ @Override
+ public V get(String key) throws IOException {
+ return null; // TODO
+ }
+
+ @Override
+ public DataStore set(String key, V value) throws IOException {
+ return null; // TODO
+ }
+
+ @Override
+ public DataStore clear() throws IOException {
+ return null; // TODO
+ }
+
+ @Override
+ public DataStore delete(String key) throws IOException {
+ return null; // TODO
+ }
+
+}
diff --git a/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStoreFactory.java b/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStoreFactory.java
new file mode 100755
index 000000000..371a5d05b
--- /dev/null
+++ b/glass-mirror/src/main/java/com/google/zxing/client/glass/store/MySQLDataStoreFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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.glass.store;
+
+import com.google.api.client.util.store.AbstractDataStoreFactory;
+import com.google.api.client.util.store.DataStore;
+
+import java.io.Serializable;
+
+final class MySQLDataStoreFactory extends AbstractDataStoreFactory {
+
+ @Override
+ protected DataStore createDataStore(String id) {
+ return new MySQLDataStore<>(this, id);
+ }
+
+}
diff --git a/glass-mirror/src/main/resources/Strings.properties b/glass-mirror/src/main/resources/Strings.properties
new file mode 100755
index 000000000..0c2c86193
--- /dev/null
+++ b/glass-mirror/src/main/resources/Strings.properties
@@ -0,0 +1,8 @@
+app.title=Barcode Scanner for Google Glass
+setup.welcome.title=Welcome to {0}
+setup.welcome.text=To read a barcode, share a photo of it with the contact called '{0}'
+scan.result.notfound.title=No Barcode Found
+scan.result.notfound.text=Sorry, no barcode could be found it that image. Try sharing another image.
+scan.result.error.title=Error
+scan.result.error.text=An error occurred while looking for a barcode: {0}
+scan.result.success.title={0} Barcode
\ No newline at end of file
diff --git a/glass-mirror/src/main/resources/test.jpg b/glass-mirror/src/main/resources/test.jpg
new file mode 100755
index 000000000..ca9ceef7f
Binary files /dev/null and b/glass-mirror/src/main/resources/test.jpg differ
diff --git a/glass-mirror/src/main/webapp/WEB-INF/web.xml b/glass-mirror/src/main/webapp/WEB-INF/web.xml
new file mode 100755
index 000000000..87e1120cb
--- /dev/null
+++ b/glass-mirror/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+ CLIENT_ID
+ TODO
+
+
+ CLIENT_SECRET
+ TODO
+
+
+
+ BASE_URL
+ http://example.org/todo
+
+
+
+ SUBSCRIPTION_VERIFY_TOKEN
+ TODO
+
+
+
+ OAuth2CallbackServlet
+ com.google.zxing.client.glass.NewUserSetupServlet
+
+
+ NotificationServlet
+ com.google.zxing.client.glass.NotificationServlet
+
+
+
+ OAuth2CallbackServlet
+ /oauth2callback
+ /start
+
+
+ NotificationServlet
+ /notification
+
+
+
+ java.lang.Throwable
+ /error.jspx
+
+
+ 400
+ /error.jspx
+
+
+ 401
+ /error.jspx
+
+
+ 404
+ /error.jspx
+
+
+ 405
+ /error.jspx
+
+
+ 500
+ /error.jspx
+
+
+ 501
+ /error.jspx
+
+
+ 503
+ /error.jspx
+
+
+
diff --git a/glass-mirror/src/main/webapp/error.jspx b/glass-mirror/src/main/webapp/error.jspx
new file mode 100755
index 000000000..cb8cfb872
--- /dev/null
+++ b/glass-mirror/src/main/webapp/error.jspx
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+]]>
+
+
+Error
+
+
+
+Error ${pageContext.errorData.statusCode} : ${pageContext.errorData.requestURI}
+
+
+Throwable t = pageContext.getErrorData().getThrowable();
+if (t != null) {
+ t.printStackTrace(new PrintWriter(out));
+}
+
+
+
+
+
\ No newline at end of file
diff --git a/glass-mirror/src/main/webapp/glass/index.jspx b/glass-mirror/src/main/webapp/glass/index.jspx
new file mode 100755
index 000000000..d15710ee4
--- /dev/null
+++ b/glass-mirror/src/main/webapp/glass/index.jspx
@@ -0,0 +1,31 @@
+
+
+
+
+
+]]>
+
+
+
+ Barcode Scanner for Google Glass
+
+
+
+ Success
+
+
+
diff --git a/glass-mirror/src/main/webapp/glass/style.css b/glass-mirror/src/main/webapp/glass/style.css
new file mode 100755
index 000000000..6eae79835
--- /dev/null
+++ b/glass-mirror/src/main/webapp/glass/style.css
@@ -0,0 +1,3 @@
+body {background-color:black;margin:0;padding:0}
+body,th,td,p {font-family:Helvetica,Arial,sans-serif;color:white}
+td {vertical-align:top}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 137078ee0..70629cc63 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,7 @@
zxingorg
android
androidtest
+ glass-mirror