Refactored the MonochromeBitmapSource class hierarchy into LuminanceSource, Binarizer...
[zxing.git] / rim / src / com / google / zxing / client / rim / ZXingLMMainScreen.java
1 /*\r
2  * Copyright 2008 ZXing authors\r
3  *\r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *      http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 package com.google.zxing.client.rim;\r
18 \r
19 import com.google.zxing.BinaryBitmap;\r
20 import com.google.zxing.DecodeHintType;\r
21 import com.google.zxing.LuminanceSource;\r
22 import com.google.zxing.MultiFormatReader;\r
23 import com.google.zxing.Reader;\r
24 import com.google.zxing.ReaderException;\r
25 import com.google.zxing.Result;\r
26 import com.google.zxing.common.GlobalHistogramBinarizer;\r
27 import com.google.zxing.client.j2me.LCDUIImageLuminanceSource;\r
28 import com.google.zxing.client.rim.persistence.AppSettings;\r
29 import com.google.zxing.client.rim.persistence.history.DecodeHistory;\r
30 import com.google.zxing.client.rim.persistence.history.DecodeHistoryItem;\r
31 import com.google.zxing.client.rim.util.Log;\r
32 import com.google.zxing.client.rim.util.ReasonableTimer;\r
33 import com.google.zxing.client.rim.util.URLDecoder;\r
34 import net.rim.blackberry.api.browser.Browser;\r
35 import net.rim.blackberry.api.browser.BrowserSession;\r
36 import net.rim.device.api.ui.DrawStyle;\r
37 import net.rim.device.api.ui.Field;\r
38 import net.rim.device.api.ui.FieldChangeListener;\r
39 import net.rim.device.api.ui.Manager;\r
40 import net.rim.device.api.ui.UiApplication;\r
41 import net.rim.device.api.ui.component.ButtonField;\r
42 import net.rim.device.api.ui.component.Dialog;\r
43 import net.rim.device.api.ui.component.LabelField;\r
44 import net.rim.device.api.ui.container.DialogFieldManager;\r
45 import net.rim.device.api.ui.container.MainScreen;\r
46 import net.rim.device.api.ui.container.PopupScreen;\r
47 import net.rim.device.api.ui.container.VerticalFieldManager;\r
48 \r
49 import javax.microedition.io.Connector;\r
50 import javax.microedition.io.file.FileConnection;\r
51 import javax.microedition.lcdui.Image;\r
52 import java.io.IOException;\r
53 import java.io.InputStream;\r
54 import java.util.Hashtable;\r
55 \r
56 /**\r
57  * The main appication menu screen.\r
58  *\r
59  * This code was contributed by LifeMarks.\r
60  *\r
61  * @author Matt York (matt@lifemarks.mobi)\r
62  */\r
63 final class ZXingLMMainScreen extends MainScreen {\r
64 \r
65   private final ZXingUiApplication app;\r
66   private final QRCapturedJournalListener imageListener;\r
67   private PopupScreen popup;\r
68   private final Reader reader;\r
69   private final Hashtable readerHints;\r
70 \r
71   ZXingLMMainScreen() {\r
72     super(DEFAULT_MENU | DEFAULT_CLOSE);\r
73     setTitle(new LabelField("ZXing", DrawStyle.ELLIPSIS | USE_ALL_WIDTH));\r
74     setChangeListener(null);\r
75 \r
76     Manager vfm = new VerticalFieldManager(USE_ALL_WIDTH);\r
77     FieldChangeListener buttonListener = new ButtonListener();\r
78 \r
79     //0\r
80     Field snapButton = new ButtonField("Snap", FIELD_HCENTER | ButtonField.CONSUME_CLICK | USE_ALL_WIDTH);\r
81     snapButton.setChangeListener(buttonListener);\r
82     vfm.add(snapButton);\r
83 \r
84     //1\r
85     Field historyButton = new ButtonField("History", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
86     historyButton.setChangeListener(buttonListener);\r
87     vfm.add(historyButton);\r
88 \r
89     //2\r
90     Field settingsButton = new ButtonField("Settings", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
91     settingsButton.setChangeListener(buttonListener);\r
92     vfm.add(settingsButton);\r
93 \r
94     //3\r
95     Field aboutButton = new ButtonField("About", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
96     aboutButton.setChangeListener(buttonListener);\r
97     vfm.add(aboutButton);\r
98 \r
99     //4\r
100     Field helpButton = new ButtonField("Help", FIELD_HCENTER | ButtonField.CONSUME_CLICK);\r
101     helpButton.setChangeListener(buttonListener);\r
102     vfm.add(helpButton);\r
103 \r
104     vfm.setChangeListener(null);\r
105     add(vfm);\r
106 \r
107 \r
108     app = (ZXingUiApplication) UiApplication.getUiApplication();\r
109     imageListener = new QRCapturedJournalListener(this);\r
110 \r
111     reader = new MultiFormatReader();\r
112     readerHints = new Hashtable(1);\r
113     readerHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);\r
114   }\r
115 \r
116 \r
117   /**\r
118    * Handles the newly created file. If the file is a jpg image, from the camera, the images is assumed to be\r
119    * a qrcode and decoding is attempted.\r
120    */\r
121   void imageSaved(String imagePath) {\r
122     Log.info("Image saved: " + imagePath);\r
123     app.removeFileSystemJournalListener(imageListener);\r
124     if (imagePath.endsWith(".jpg") && imagePath.indexOf("IMG") >= 0) // a blackberry camera image file\r
125     {\r
126       Log.info("imageSaved - Got file: " + imagePath);\r
127       Camera.getInstance().exit();\r
128       Log.info("camera exit finished");\r
129       app.requestForeground();\r
130 \r
131       DialogFieldManager manager = new DialogFieldManager();\r
132       popup = new PopupScreen(manager);\r
133       manager.addCustomField(new LabelField("Decoding image..."));\r
134 \r
135       app.pushScreen(popup); // original\r
136       Log.info("started progress screen.");\r
137 \r
138       Runnable fct = new FileConnectionThread(imagePath);\r
139       Log.info("Starting file connection thread.");\r
140       app.invokeLater(fct);\r
141       Log.info("Finished file connection thread.");\r
142     } else {\r
143       Log.error("Failed to locate camera image.");\r
144     }\r
145   }\r
146 \r
147   /**\r
148    * Closes the application and persists all required data.\r
149    */\r
150   public void close() {\r
151     app.removeFileSystemJournalListener(imageListener);\r
152     DecodeHistory.getInstance().persist();\r
153     super.close();\r
154   }\r
155 \r
156   /**\r
157    * This method is overriden to remove the 'save changes' dialog when exiting.\r
158    */\r
159   public boolean onSavePrompt() {\r
160     setDirty(false);\r
161     return true;\r
162   }\r
163 \r
164   /**\r
165    * Listens for selected buttons and starts the required screen.\r
166    */\r
167   private final class ButtonListener implements FieldChangeListener {\r
168     public void fieldChanged(Field field, int context) {\r
169       Log.debug("*** fieldChanged: " + field.getIndex());\r
170       switch (field.getIndex()) {\r
171         case 0: // snap\r
172           try {\r
173             app.addFileSystemJournalListener(imageListener);\r
174             Camera.getInstance().invoke(); // start camera\r
175             return;\r
176           }\r
177           catch (Exception e) {\r
178             Log.error("!!! Problem invoking camera.!!!: " + e);\r
179           }\r
180           break;\r
181         case 1: // history\r
182           app.pushScreen(new HistoryScreen());\r
183           break;\r
184         case 2: // settings\r
185           app.pushScreen(new SettingsScreen());\r
186           break;\r
187         case 3: //about\r
188           app.pushScreen(new AboutScreen());\r
189           break;\r
190         case 4: //help\r
191           app.pushScreen(new HelpScreen());\r
192           break;\r
193       }\r
194     }\r
195 \r
196   }\r
197 \r
198   /**\r
199    * Thread that decodes the newly created image. If the image is successfully decoded and the data is a URL,\r
200    * the browser is invoked and pointed to the given URL.\r
201    */\r
202   private final class FileConnectionThread implements Runnable {\r
203 \r
204     private final String imagePath;\r
205 \r
206     private FileConnectionThread(String imagePath) {\r
207       this.imagePath = imagePath;\r
208     }\r
209 \r
210     public void run() {\r
211       FileConnection file = null;\r
212       InputStream is = null;\r
213       Image capturedImage = null;\r
214       try {\r
215         file = (FileConnection) Connector.open("file://" + imagePath, Connector.READ_WRITE);\r
216         is = file.openInputStream();\r
217         capturedImage = Image.createImage(is);\r
218       } catch (IOException e) {\r
219         Log.error("Problem creating image: " + e);\r
220         removeProgressBar();\r
221         invalidate();\r
222         showMessage("An error occured processing the image.");\r
223         return;\r
224       } finally {\r
225         try {\r
226           if (is != null) {\r
227             is.close();\r
228           }\r
229           if (file != null && file.exists()) {\r
230             if (file.isOpen()) {\r
231               file.close();\r
232             }\r
233             file.delete();\r
234             Log.info("Deleted image file.");\r
235           }\r
236         } catch (IOException ioe) {\r
237           Log.error("Error while closing file: " + ioe);\r
238         }\r
239       }\r
240 \r
241       if (capturedImage != null) {\r
242         Log.info("Got image...");\r
243         LuminanceSource source = new LCDUIImageLuminanceSource(capturedImage);\r
244         BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));\r
245         Result result;\r
246         ReasonableTimer decodingTimer = null;\r
247         try {\r
248           decodingTimer = new ReasonableTimer();\r
249           Log.info("Attempting to decode image...");\r
250           result = reader.decode(bitmap, readerHints);\r
251           decodingTimer.finished();\r
252         } catch (ReaderException e) {\r
253           Log.error("Could not decode image: " + e);\r
254           decodingTimer.finished();\r
255           removeProgressBar();\r
256           invalidate();\r
257           boolean showResolutionMsg =\r
258                   !AppSettings.getInstance().getBooleanItem(AppSettings.SETTING_CAM_RES_MSG).booleanValue();\r
259           if (showResolutionMsg) {\r
260             showMessage("A QR Code was not found in the image. " +\r
261                         "We detected that the decoding process took quite a while. " +\r
262                         "It will be much faster if you decrease your camera's resolution (640x480).");\r
263           } else {\r
264             showMessage("A QR Code was not found in the image.");\r
265           }\r
266           return;\r
267         }\r
268         if (result != null) {\r
269           String resultText = result.getText();\r
270           Log.info("result: " + resultText);\r
271           if (isURI(resultText)) {\r
272             resultText = URLDecoder.decode(resultText);\r
273             removeProgressBar();\r
274             invalidate();\r
275             if (!decodingTimer.wasResonableTime() &&\r
276                 !AppSettings.getInstance().getBooleanItem(AppSettings.SETTING_CAM_RES_MSG).booleanValue()) {\r
277               showMessage("We detected that the decoding process took quite a while. " +\r
278                           "It will be much faster if you decrease your camera's resolution (640x480).");\r
279             }\r
280             DecodeHistory.getInstance().addHistoryItem(new DecodeHistoryItem(resultText));\r
281             invokeBrowser(resultText);\r
282             return;\r
283           }\r
284         } else {\r
285           removeProgressBar();\r
286           invalidate();\r
287           showMessage("A QR Code was not found in the image.");\r
288           return;\r
289         }\r
290 \r
291       }\r
292 \r
293       removeProgressBar();\r
294       invalidate();\r
295     }\r
296 \r
297     /**\r
298      * Quick check to see if the result of decoding the qr code was a valid uri.\r
299      */\r
300     private boolean isURI(String uri) {\r
301       return uri.startsWith("http://");\r
302     }\r
303 \r
304     /**\r
305      * Invokes the web browser and browses to the given uri.\r
306      */\r
307     private void invokeBrowser(String uri) {\r
308       BrowserSession browserSession = Browser.getDefaultSession();\r
309       browserSession.displayPage(uri);\r
310     }\r
311 \r
312     /**\r
313      * Syncronized version of removing progress dialog.\r
314      * NOTE: All methods accessing the gui that are in seperate threads should syncronize on app.getEventLock()\r
315      */\r
316     private void removeProgressBar() {\r
317       synchronized (app.getAppEventLock()) {\r
318         if (popup != null) {\r
319           app.popScreen(popup);\r
320         }\r
321       }\r
322     }\r
323 \r
324     /**\r
325      * Syncronized version of showing a message dialog.\r
326      * NOTE: All methods accessing the gui that are in seperate threads should syncronize on app.getEventLock()\r
327      */\r
328     private void showMessage(String message) {\r
329       synchronized (app.getAppEventLock()) {\r
330         Dialog.alert(message);\r
331       }\r
332     }\r
333   }\r
334 \r
335 }\r