package camera;
import com.buglabs.application.AbstractServiceTracker;
import com.buglabs.application.MainApplicationThread;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Image;
import java.awt.Label;
import java.awt.Rectangle;
import java.io.File;
import java.awt.Toolkit;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.*;
import com.buglabs.bug.base.pub.ITimeProvider;
import com.buglabs.bug.base.pub.IBaseAudioPlayer;
import com.buglabs.bug.module.camera.pub.ICameraButtonEventProvider;
import com.buglabs.bug.module.camera.pub.ICameraDevice;
import com.buglabs.bug.module.camera.pub.ICameraModuleControl;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.device.ButtonEvent;
import com.buglabs.device.IButtonEventListener;
import com.buglabs.device.IButtonEventProvider;
import org.apache.sanselan.formats.tiff.write.TiffOutputSet;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;
/**
* CameraApplication Main application thread. The run method is invoked
* by the applications service tracker when all services are accounted for.
*
*/
public class CameraApplication
extends MainApplicationThread
implements
IButtonEventListener {
// TODO: Shouldn't need this...
private static final boolean VIRTUAL_BUG = false;
private static final Rectangle PREVIEW_BOUNDS = new Rectangle(30, 15, 260, 195);
private static final Rectangle STATUS_BOUNDS = new Rectangle(30, 210, 140, 30);
private static final Rectangle MODE_BOUNDS = new Rectangle(170, 210, 120, 30);
private static final int BG_COLOR = 0xB61C00; // the red of the Bug matchbox theme
private static final String REVIEW_MODE_LABEL = "Image";
private static final String SLIDESHOW_LABEL = "Slide";
private static final long SLIDESHOW_INTERVAL = 5000;
// TODO: Make these use the Command pattern instead of cheesey ints
private static final int TOGGLE_MODE_COMMAND = 1;
private static final int TAKE_A_PICTURE_COMMAND = 2;
private static final int NEXT_IMAGE_COMMAND = 3;
private static final int PREV_IMAGE_COMMAND = 4;
private static final int TOGGLE_SLIDESHOW = 5;
private final SimpleDateFormat timestampFormat= new SimpleDateFormat("yyyyMMdd-HHmmss");
private final AbstractServiceTracker serviceTracker;
private boolean ran;
private Frame frame;
private PreviewPanel previewPanel;
private Label statusArea;
private Label modeArea;
private LogService log;
private Store store;
private ICameraDevice iCameraDevice;
private ICameraButtonEventProvider iCameraButtonEventProvider;
private IButtonEventProvider baseIButtonEventProvider;
private ITimeProvider iTimeProvider;
private IBaseAudioPlayer iBaseAudioPlayer;
private File shutterSound;
private Toolkit toolkit;
private LivePreviewControl livePreviewControl;
private boolean inReviewMode = false; // start in live mode
private List commands = new LinkedList();
private int displayedImageIndex = -1;
private ICameraModuleControl iCameraModuleControl;
private boolean isSlideshowActive = false;
private Timer slideshowTimer;
private ExifDataHandler exifDataHandler;
public CameraApplication(AbstractServiceTracker serviceTracker) {
this.serviceTracker = serviceTracker;
ran = false;
}
/**
* Informs the caller whether this thread ran.
*/
public boolean getRan() {
return ran;
}
private void ran() {
ran = true;
}
/**
* This method is invoked as a result of all services
* becoming available for the application. The list of services is
* obtained from the getServices() method.
*/
public void run() {
log = getLogService();
this.store = new Store(serviceTracker.getBundleContext(), getLogService());
// get all our service handles
iCameraModuleControl = getICameraModuleControl();
iCameraButtonEventProvider = getICameraButtonEventProvider();
baseIButtonEventProvider = getIButtonEventProvider();
iCameraDevice = getICameraDevice();
iTimeProvider = getITimeProvider();
iBaseAudioPlayer = getIBaseAudioPlayer();
toolkit = Toolkit.getDefaultToolkit();
exifDataHandler = new ExifDataHandler(log, serviceTracker);
try
{
final String TEMP_FILENAME = "/tmp/camera_shutter.wav";
final URL url = serviceTracker.getBundleContext().getBundle().getResource("resources/shutter.wav");
final InputStream shutterInputStream = url.openStream();
final OutputStream shutterOutputStream = new FileOutputStream(TEMP_FILENAME);
int c;
while ((c = shutterInputStream.read()) != -1) {
shutterOutputStream.write(c);
}
shutterOutputStream.close();
shutterSound = new File(TEMP_FILENAME);
}
catch (IOException e) {
log.log(LogService.LOG_ERROR, "Unable to read shutter wave file: " + e.getMessage());
shutterSound = null;
}
// open our kimono
showUI();
// assume here that the frame (that is, the ImagePanel's parent) is
// on-screen from 0,0 (since LivePreviewControl is going to do its
// thing outside the control of AWT).
livePreviewControl = new LivePreviewControl(log, iCameraDevice);
// start with live preview running
livePreviewControl.enableLivePreview();
// now we're all set up, let the events commence
iCameraButtonEventProvider.addListener(this);
baseIButtonEventProvider.addListener(this);
log.log(LogService.LOG_INFO, "Running: CameraApplication");
//Main application loop the run method will commence
//once all service dependencies are satisfied.
Integer command = null;
while(!tearDownRequested) {
while ((command = nextCommand()) != null) {
log.log(LogService.LOG_DEBUG, "Processing command " + command);
switch (command.intValue()) {
case TOGGLE_MODE_COMMAND:
toggleMode();
break;
case TAKE_A_PICTURE_COMMAND:
takeAPicture();
break;
case NEXT_IMAGE_COMMAND:
nextImage();
break;
case PREV_IMAGE_COMMAND:
prevImage();
break;
case TOGGLE_SLIDESHOW:
toggleSlideshow();
break;
default:
log.log(LogService.LOG_ERROR, "Unrecognized command " + command);
break;
}
// sleep for a short time in case the user is mashing
// buttons, so that we can deal with them all at before
// displaying an image
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
}
}
// try and avoid thrashing on user mashing the prev/next buttons,
// but not actually displaying an image until they let go, and
// then only if it was different
if (inReviewMode) {
displayReviewImage();
}
try {
Thread.sleep(60000);
}
catch (InterruptedException e) {
log.log(LogService.LOG_INFO, "Interrupted");
}
}
stopSlideshow();
destroyUI();
iCameraButtonEventProvider.removeListener(this);
iCameraButtonEventProvider = null;
iCameraModuleControl = null;
iCameraDevice = null;
toolkit = null;
log.log(LogService.LOG_INFO, "CameraApplication stopped");
/**
* Let the service tracker know we ran.
*/
ran();
}
/**
* Provides a list of service names that this application depends on.
*
*/
public List getServices() {
List services = new ArrayList();
services.add("com.buglabs.bug.module.camera.pub.ICameraButtonEventProvider");
services.add("com.buglabs.bug.module.camera.pub.ICameraDevice");
services.add("com.buglabs.bug.module.camera.pub.ICameraModuleControl");
services.add("com.buglabs.bug.module.lcd.pub.IModuleDisplay");
services.add("com.buglabs.bug.base.pub.ITimeProvider");
services.add("com.buglabs.device.IButtonEventProvider");
if (!VIRTUAL_BUG) {
services.add("org.osgi.service.log.LogService");
services.add("com.buglabs.bug.base.pub.IBaseAudioPlayer");
}
return services;
}
/**
* Queries the service provider for ICameraButtonEventProvider.
*
* @return a handle to the a(n) ICameraButtonEventProvider service.
*/
private ICameraButtonEventProvider getICameraButtonEventProvider() {
return (ICameraButtonEventProvider) serviceTracker.getService(ICameraButtonEventProvider.class);
}
/**
* Queries the service provider for ICameraDevice.
*
* @return a handle to the a(n) ICameraDevice service.
*/
private ICameraDevice getICameraDevice() {
return (ICameraDevice) serviceTracker.getService(ICameraDevice.class);
}
/**
* Queries the service provider for ICameraModuleControl.
*
* @return a handle to the a(n) ICameraModuleControl service.
*/
private ICameraModuleControl getICameraModuleControl() {
return (ICameraModuleControl) serviceTracker.getService(ICameraModuleControl.class);
}
/**
* Queries the service provider for IModuleDisplay.
*
* @return a handle to the a(n) IModuleDisplay service.
*/
private IModuleDisplay getIModuleDisplay() {
return (IModuleDisplay) serviceTracker.getService(IModuleDisplay.class);
}
/**
* Queries the service provider for ITimeProvider.
*
* @return a handle to the a(n) ITimeProvider service.
*/
private ITimeProvider getITimeProvider() {
return (ITimeProvider) serviceTracker.getService(ITimeProvider.class);
}
/**
* Queries the service provider for IButtonEventProvider (ie the base buttons).
*
* @return a handle to the a(n) IButtonEventProvider service.
*/
private IButtonEventProvider getIButtonEventProvider() {
return (IButtonEventProvider) serviceTracker.getService(IButtonEventProvider.class);
}
/**
* Queries the service provider for IBaseAudioPlayer.
*
* @return a handle to the a(n) IBaseAudioPlayer service.
*/
private IBaseAudioPlayer getIBaseAudioPlayer() {
return (IBaseAudioPlayer) serviceTracker.getService(IBaseAudioPlayer.class);
}
/**
* Queries the service provider for LogService.
*
* @return a handle to the a(n) LogService service.
*/
private LogService getLogService() {
if (VIRTUAL_BUG) {
return new LogService() {
public void log(int level, String message) {
System.out.println(message);
}
public void log(int level, String message, Throwable exception) {
System.out.println(message);
}
public void log(ServiceReference sr, int level, String message) {
System.out.println(message);
}
public void log(ServiceReference sr, int level, String message,
Throwable exception) {
System.out.println(message);
}
};
}
else {
return (LogService) serviceTracker.getService(LogService.class);
}
}
/**
* Build the frame that is our UI and show it.
*/
private void showUI() {
frame = getIModuleDisplay().getFrame();
// set the title so the Matchbox switcher will show something useful
frame.setTitle("Camera");
// we're going it along on layout, as we have external
// forces (the JNI-triggered live preview) that we need
// absolute positioning to work with properly.
frame.setLayout(null);
System.out.println("Frame bounds: " + frame.getBounds());
frame.setBackground(new Color(BG_COLOR));
statusArea = new Label();
statusArea.setBounds(STATUS_BOUNDS);
statusArea.setBackground(Color.LIGHT_GRAY);
statusArea.setAlignment(Label.LEFT);
frame.add(statusArea);
statusArea.setVisible(inReviewMode);
modeArea = new Label();
modeArea.setBounds(MODE_BOUNDS);
modeArea.setBackground(Color.LIGHT_GRAY);
modeArea.setAlignment(Label.RIGHT);
modeArea.setText(REVIEW_MODE_LABEL);
frame.add(modeArea);
modeArea.setVisible(inReviewMode);
previewPanel = new PreviewPanel(log);
previewPanel.setBounds(PREVIEW_BOUNDS);
previewPanel.setBackground(Color.BLACK);
frame.add(previewPanel);
previewPanel.setVisible(inReviewMode);
log.log(LogService.LOG_DEBUG, "Showing frame now");
frame.show();
}
/**
* Clean up the stuff what we created
*/
private void destroyUI() {
frame.dispose();
frame = null;
previewPanel = null;
statusArea = null;
modeArea = null;
}
/*
* Capture events that were sent from the camera
*
* @see com.buglabs.device.IButtonEventListener#buttonEvent(com.buglabs.device.ButtonEvent)
*/
public void buttonEvent(ButtonEvent event) {
// note that this method is called on the event provider's context,
// so we can't do time-intensive work in here. For example, calling
// takeAPicture here instead of waking the main app thread up to
// do it prevents the screen from being updated until this call,
// and hence our call to takeAPicture, finishes.
final int value = event.getRawValue();
log.log(LogService.LOG_DEBUG, "raw event: " + event.getRawValue());
if (event.getAction() == ButtonEvent.KEY_UP) {
if (value == ButtonEvent.BUTTON_CAMERA_ZOOM_OUT) {
// TODO: implement zoom out
log.log(LogService.LOG_DEBUG, "ZOOM OUT Pressed");
}
if (value == ButtonEvent.BUTTON_CAMERA_ZOOM_IN) {
// TODO: implement zoom in
log.log(LogService.LOG_DEBUG, "ZOOM IN Pressed");
}
if (value == ButtonEvent.BUTTON_CAMERA_SHUTTER) {
log.log(LogService.LOG_DEBUG, "SHUTTER Pressed");
if (!inReviewMode) {
// wake up our main loop, which will make it take a picture
// (we can't take one here as we're in the event loop thread
// and it takes too long)
queueCommand(TAKE_A_PICTURE_COMMAND);
}
}
if (value == ButtonEvent.BUTTON_HOTKEY_1) {
log.log(LogService.LOG_DEBUG, "HOTKEY 1 Pressed");
if (inReviewMode) {
queueCommand(TOGGLE_SLIDESHOW);
}
else if (VIRTUAL_BUG) {
// overload HK1 on virtual bug in picture mode to act as shutter
queueCommand(TAKE_A_PICTURE_COMMAND);
}
}
if (value == ButtonEvent.BUTTON_HOTKEY_2) {
log.log(LogService.LOG_DEBUG, "HOTKEY 2 Pressed");
if (inReviewMode) {
queueCommand(PREV_IMAGE_COMMAND);
}
}
if (value == ButtonEvent.BUTTON_HOTKEY_3) {
log.log(LogService.LOG_DEBUG, "HOTKEY 3 Pressed");
if (inReviewMode) {
queueCommand(NEXT_IMAGE_COMMAND);
}
}
if (value == ButtonEvent.BUTTON_HOTKEY_4) {
log.log(LogService.LOG_DEBUG, "HOTKEY 4 Pressed");
queueCommand(TOGGLE_MODE_COMMAND);
}
}
}
private void makeShutterSound() {
if ((shutterSound != null) && (iBaseAudioPlayer != null)) {
try {
iBaseAudioPlayer.play(new FileInputStream(shutterSound));
} catch (IOException e) {
log.log(LogService.LOG_WARNING, "Ignoring error playing shutter sound: " + e.getMessage());
}
}
}
private void takeAPicture() {
log.log(LogService.LOG_DEBUG, "takeAPicture");
makeShutterSound();
final byte [] imageData;
try {
iCameraModuleControl.setLEDFlash(true);
imageData = iCameraDevice.getImage();
iCameraModuleControl.setLEDFlash(false);
} catch (IOException e) {
e.printStackTrace();
log.log(LogService.LOG_ERROR, "Error playing with flash: " + e.getMessage());
return;
}
final Date timestamp = iTimeProvider.getTime();
final TiffOutputSet exifData = exifDataHandler.createExifData(timestamp);
final StringBuffer sb = new StringBuffer();
timestampFormat.format(timestamp, sb, new FieldPosition(0));
store.saveImage(imageData, sb.toString(), exifData);
}
private void toggleMode() {
log.log(LogService.LOG_DEBUG, "toggleMode");
if (inReviewMode) {
if (isSlideshowActive) {
stopSlideshow();
}
log.log(LogService.LOG_INFO, "switching to live mode");
statusArea.setVisible(false);
modeArea.setVisible(false);
previewPanel.setVisible(false);
livePreviewControl.enableLivePreview();
// make sure we'll redisplay on the way back
displayedImageIndex = -1;
}
else {
log.log(LogService.LOG_INFO, "switching to review mode");
livePreviewControl.disableLivePreview();
statusArea.setVisible(true);
modeArea.setVisible(true);
previewPanel.setVisible(true);
store.refresh();
displayReviewImage();
}
inReviewMode = !inReviewMode;
}
private void displayReviewImage() {
log.log(LogService.LOG_DEBUG, "displayReviewImage");
final File file = store.getImageFile();
if (file == null) {
statusArea.setText("no saved images");
updateModeInfo();
}
else if (displayedImageIndex == store.getImageIndex()) {
// no need to redisplay it
}
else {
displayedImageIndex = store.getImageIndex();
final byte [] imageData = new byte[(int) file.length()];
try {
final FileInputStream in = new FileInputStream(file);
in.read(imageData);
final Image image = toolkit.createImage(imageData);
previewPanel.setImage(image);
statusArea.setText(file.getName());
updateModeInfo();
displayedImageIndex = store.getImageIndex();
}
catch (IOException e) {
e.printStackTrace();
statusArea.setText("unable to read " + file.getName());
}
}
}
private void updateModeInfo() {
if (store.getImageCount() == 0) {
modeArea.setText("empty");
}
else {
modeArea.setText((isSlideshowActive ? SLIDESHOW_LABEL : REVIEW_MODE_LABEL) +
" " + (store.getImageIndex()+1) +
" of " + store.getImageCount());
}
}
private void prevImage() {
store.prevImage();
// update the info, but don't actually
// display the new image yet; we do that
// when we reach quiescence to avoid unnecessary
// chewing of cycles and memory
updateModeInfo();
}
private void nextImage() {
store.nextImage();
// update the info, but don't actually
// display the new image yet; we do that
// when we reach quiescence to avoid unnecessary
// chewing of cycles and memory
updateModeInfo();
}
private synchronized void queueCommand(int command) {
commands.add(new Integer(command));
// wake up our main loop (in run) to process the command
interrupt();
}
private synchronized Integer nextCommand() {
if (commands.isEmpty()) {
return null;
}
final Integer command = (Integer) commands.get(0);
commands.remove(0);
return command;
}
private void toggleSlideshow() {
if (isSlideshowActive) {
stopSlideshow();
}
else {
startSlideshow();
}
}
private void startSlideshow() {
log.log(LogService.LOG_DEBUG, "Starting slideshow");
isSlideshowActive = true;
slideshowTimer = new Timer();
slideshowTimer.scheduleAtFixedRate(new TimerTask() {
public void run() {
if (CameraApplication.this.inReviewMode) {
CameraApplication.this.nextImage();
CameraApplication.this.displayReviewImage();
}
}},
0, SLIDESHOW_INTERVAL);
}
private void stopSlideshow() {
log.log(LogService.LOG_DEBUG, "Stopping slideshow");
isSlideshowActive = false;
if (slideshowTimer != null) {
slideshowTimer.cancel();
slideshowTimer = null;
updateModeInfo();
}
}
}