Initial checkin of RIM client from LifeMarks, after initial refactorings and style...
[zxing.git] / rim / src / com / google / zxing / client / rim / ZXingLMMainScreen.java
diff --git a/rim/src/com/google/zxing/client/rim/ZXingLMMainScreen.java b/rim/src/com/google/zxing/client/rim/ZXingLMMainScreen.java
new file mode 100644 (file)
index 0000000..1e8a2f5
--- /dev/null
@@ -0,0 +1,331 @@
+/*\r
+ * Copyright 2008 ZXing authors\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.google.zxing.client.rim;\r
+\r
+import com.google.zxing.DecodeHintType;\r
+import com.google.zxing.MonochromeBitmapSource;\r
+import com.google.zxing.MultiFormatReader;\r
+import com.google.zxing.Reader;\r
+import com.google.zxing.ReaderException;\r
+import com.google.zxing.Result;\r
+import com.google.zxing.client.j2me.LCDUIImageMonochromeBitmapSource;\r
+import com.google.zxing.client.rim.persistence.AppSettings;\r
+import com.google.zxing.client.rim.persistence.history.DecodeHistory;\r
+import com.google.zxing.client.rim.persistence.history.DecodeHistoryItem;\r
+import com.google.zxing.client.rim.util.Log;\r
+import com.google.zxing.client.rim.util.ReasonableTimer;\r
+import com.google.zxing.client.rim.util.URLDecoder;\r
+import net.rim.blackberry.api.browser.Browser;\r
+import net.rim.blackberry.api.browser.BrowserSession;\r
+import net.rim.device.api.ui.DrawStyle;\r
+import net.rim.device.api.ui.Field;\r
+import net.rim.device.api.ui.FieldChangeListener;\r
+import net.rim.device.api.ui.Manager;\r
+import net.rim.device.api.ui.UiApplication;\r
+import net.rim.device.api.ui.component.ButtonField;\r
+import net.rim.device.api.ui.component.Dialog;\r
+import net.rim.device.api.ui.component.LabelField;\r
+import net.rim.device.api.ui.container.DialogFieldManager;\r
+import net.rim.device.api.ui.container.MainScreen;\r
+import net.rim.device.api.ui.container.PopupScreen;\r
+import net.rim.device.api.ui.container.VerticalFieldManager;\r
+\r
+import javax.microedition.io.Connector;\r
+import javax.microedition.io.file.FileConnection;\r
+import javax.microedition.lcdui.Image;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.util.Hashtable;\r
+\r
+/**\r
+ * The main appication menu screen.\r
+ *\r
+ * This code was contributed by LifeMarks.\r
+ *\r
+ * @author Matt York (matt@lifemarks.mobi)\r
+ */\r
+final class ZXingLMMainScreen extends MainScreen {\r
+\r
+  private final ZXingUiApplication app;\r
+  private final QRCapturedJournalListener imageListener;\r
+  private PopupScreen popup;\r
+  private final Reader reader;\r
+  private final Hashtable readerHints;\r
+\r
+  ZXingLMMainScreen() {\r
+    super(DEFAULT_MENU | DEFAULT_CLOSE);\r
+    setTitle(new LabelField("ZXing", DrawStyle.ELLIPSIS | USE_ALL_WIDTH));\r
+    setChangeListener(null);\r
+\r
+    Manager vfm = new VerticalFieldManager(USE_ALL_WIDTH);\r
+    FieldChangeListener buttonListener = new ButtonListener();\r
+\r
+    //0\r
+    Field snapButton = new ButtonField("Snap", FIELD_HCENTER | ButtonField.CONSUME_CLICK | USE_ALL_WIDTH);\r
+    snapButton.setChangeListener(buttonListener);\r
+    vfm.add(snapButton);\r
+\r
+    //1\r
+    Field historyButton = new ButtonField("History", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
+    historyButton.setChangeListener(buttonListener);\r
+    vfm.add(historyButton);\r
+\r
+    //2\r
+    Field settingsButton = new ButtonField("Settings", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
+    settingsButton.setChangeListener(buttonListener);\r
+    vfm.add(settingsButton);\r
+\r
+    //3\r
+    Field aboutButton = new ButtonField("About", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
+    aboutButton.setChangeListener(buttonListener);\r
+    vfm.add(aboutButton);\r
+\r
+    //4\r
+    Field helpButton = new ButtonField("Help", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
+    helpButton.setChangeListener(buttonListener);\r
+    vfm.add(helpButton);\r
+\r
+    vfm.setChangeListener(null);\r
+    add(vfm);\r
+\r
+\r
+    app = (ZXingUiApplication) UiApplication.getUiApplication();\r
+    imageListener = new QRCapturedJournalListener(this);\r
+\r
+    reader = new MultiFormatReader();\r
+    readerHints = new Hashtable(1);\r
+    readerHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);\r
+  }\r
+\r
+\r
+  /**\r
+   * Handles the newly created file. If the file is a jpg image, from the camera, the images is assumed to be\r
+   * a qrcode and decoding is attempted.\r
+   */\r
+  void imageSaved(String imagePath) {\r
+    Log.info("Image saved: " + imagePath);\r
+    app.removeFileSystemJournalListener(imageListener);\r
+    if (imagePath.endsWith(".jpg") && imagePath.indexOf("IMG") >= 0) // a blackberry camera image file\r
+    {\r
+      Log.info("imageSaved - Got file: " + imagePath);\r
+      Camera.getInstance().exit();\r
+      Log.info("camera exit finished");\r
+      app.requestForeground();\r
+\r
+      DialogFieldManager manager = new DialogFieldManager();\r
+      popup = new PopupScreen(manager);\r
+      manager.addCustomField(new LabelField("Decoding image..."));\r
+\r
+      app.pushScreen(popup); // original\r
+      Log.info("started progress screen.");\r
+\r
+      Runnable fct = new FileConnectionThread(imagePath);\r
+      Log.info("Starting file connection thread.");\r
+      app.invokeLater(fct);\r
+      Log.info("Finished file connection thread.");\r
+    } else {\r
+      Log.error("Failed to locate camera image.");\r
+    }\r
+  }\r
+\r
+  /**\r
+   * Closes the application and persists all required data.\r
+   */\r
+  public void close() {\r
+    app.removeFileSystemJournalListener(imageListener);\r
+    DecodeHistory.getInstance().persist();\r
+    super.close();\r
+  }\r
+\r
+  /**\r
+   * This method is overriden to remove the 'save changes' dialog when exiting.\r
+   */\r
+  public boolean onSavePrompt() {\r
+    setDirty(false);\r
+    return true;\r
+  }\r
+\r
+  /**\r
+   * Listens for selected buttons and starts the required screen.\r
+   */\r
+  private final class ButtonListener implements FieldChangeListener {\r
+    public void fieldChanged(Field field, int context) {\r
+      Log.debug("*** fieldChanged: " + field.getIndex());\r
+      switch (field.getIndex()) {\r
+        case 0: // snap\r
+          try {\r
+            app.addFileSystemJournalListener(imageListener);\r
+            Camera.getInstance().invoke(); // start camera\r
+            return;\r
+          }\r
+          catch (Exception e) {\r
+            Log.error("!!! Problem invoking camera.!!!: " + e);\r
+          }\r
+          break;\r
+        case 1: // history\r
+          app.pushScreen(new HistoryScreen());\r
+          break;\r
+        case 2: // settings\r
+          app.pushScreen(new SettingsScreen());\r
+          break;\r
+        case 3: //about\r
+          app.pushScreen(new AboutScreen());\r
+          break;\r
+        case 4: //help\r
+          app.pushScreen(new HelpScreen());\r
+          break;\r
+      }\r
+    }\r
+\r
+  }\r
+\r
+  /**\r
+   * Thread that decodes the newly created image. If the image is successfully decoded and the data is a URL,\r
+   * the browser is invoked and pointed to the given URL.\r
+   */\r
+  private final class FileConnectionThread implements Runnable {\r
+\r
+    private final String imagePath;\r
+\r
+    private FileConnectionThread(String imagePath) {\r
+      this.imagePath = imagePath;\r
+    }\r
+\r
+    public void run() {\r
+      FileConnection file = null;\r
+      InputStream is = null;\r
+      Image capturedImage = null;\r
+      try {\r
+        file = (FileConnection) Connector.open("file://" + imagePath, Connector.READ_WRITE);\r
+        is = file.openInputStream();\r
+        capturedImage = Image.createImage(is);\r
+      } catch (IOException e) {\r
+        Log.error("Problem creating image: " + e);\r
+        removeProgressBar();\r
+        invalidate();\r
+        showMessage("An error occured processing the image.");\r
+        return;\r
+      } finally {\r
+        if (is != null) {\r
+          try {\r
+            is.close();\r
+          } catch (IOException ioe) {\r
+          }\r
+        }\r
+        if (file != null && file.exists()) {\r
+          if (file.isOpen()) {\r
+            //file.close();\r
+          }\r
+          //file.delete();\r
+          Log.info("Deleted image file.");\r
+        }\r
+      }\r
+\r
+      if (capturedImage != null) {\r
+        Log.info("Got image...");\r
+        MonochromeBitmapSource source = new LCDUIImageMonochromeBitmapSource(capturedImage);\r
+        Result result;\r
+        ReasonableTimer decodingTimer = null;\r
+        try {\r
+          decodingTimer = new ReasonableTimer();\r
+          Log.info("Attempting to decode image...");\r
+          result = reader.decode(source, readerHints);\r
+          decodingTimer.finished();\r
+        } catch (ReaderException e) {\r
+          Log.error("Could not decode image: " + e);\r
+          decodingTimer.finished();\r
+          removeProgressBar();\r
+          invalidate();\r
+          boolean showResolutionMsg =\r
+                  !AppSettings.getInstance().getBooleanItem(AppSettings.SETTING_CAM_RES_MSG).booleanValue();\r
+          if (showResolutionMsg) {\r
+            showMessage("A QR Code was not found in the image. " +\r
+                        "We detected that the decoding process took quite a while. " +\r
+                        "It will be much faster if you decrease your camera's resolution (640x480).");\r
+          } else {\r
+            showMessage("A QR Code was not found in the image.");\r
+          }\r
+          return;\r
+        }\r
+        if (result != null) {\r
+          String resultText = result.getText();\r
+          Log.info("result: " + resultText);\r
+          if (isURI(resultText)) {\r
+            resultText = URLDecoder.decode(resultText);\r
+            removeProgressBar();\r
+            invalidate();\r
+            if (!decodingTimer.wasResonableTime() &&\r
+                !AppSettings.getInstance().getBooleanItem(AppSettings.SETTING_CAM_RES_MSG).booleanValue()) {\r
+              showMessage("We detected that the decoding process took quite a while. " +\r
+                          "It will be much faster if you decrease your camera's resolution (640x480).");\r
+            }\r
+            DecodeHistory.getInstance().addHistoryItem(new DecodeHistoryItem(resultText));\r
+            invokeBrowser(resultText);\r
+            return;\r
+          }\r
+        } else {\r
+          removeProgressBar();\r
+          invalidate();\r
+          showMessage("A QR Code was not found in the image.");\r
+          return;\r
+        }\r
+\r
+      }\r
+\r
+      removeProgressBar();\r
+      invalidate();\r
+    }\r
+\r
+    /**\r
+     * Quick check to see if the result of decoding the qr code was a valid uri.\r
+     */\r
+    private boolean isURI(String uri) {\r
+      return uri.startsWith("http://");\r
+    }\r
+\r
+    /**\r
+     * Invokes the web browser and browses to the given uri.\r
+     */\r
+    private void invokeBrowser(String uri) {\r
+      BrowserSession browserSession = Browser.getDefaultSession();\r
+      browserSession.displayPage(uri);\r
+    }\r
+\r
+    /**\r
+     * Syncronized version of removing progress dialog.\r
+     * NOTE: All methods accessing the gui that are in seperate threads should syncronize on app.getEventLock()\r
+     */\r
+    private void removeProgressBar() {\r
+      synchronized (app.getAppEventLock()) {\r
+        if (popup != null) {\r
+          app.popScreen(popup);\r
+        }\r
+      }\r
+    }\r
+\r
+    /**\r
+     * Syncronized version of showing a message dialog.\r
+     * NOTE: All methods accessing the gui that are in seperate threads should syncronize on app.getEventLock()\r
+     */\r
+    private void showMessage(String message) {\r
+      synchronized (app.getAppEventLock()) {\r
+        Dialog.alert(message);\r
+      }\r
+    }\r
+  }\r
+\r
+}\r