Tutorial: Making a spycam
Start a webserver that serves a fresh picture taken by the on board camera on each request. Concepts covered:
- Extending stack size
- Synchronization of common background job
- Starting a webserver on the device
- Accessing the device camera
- Force the screen to stay on
- Adjusting the system volume
You should have completed the Getting started with Ruboto tutorial. This tutorial accesses the camera of the device, so it doesn't make much sense to develop on an emulator. Use a real device with a camera.
This tutorial has been tested with the following setups
Platform | JDK | ant | Ruby | ruboto | RubotoCore | Device | API level | Tester |
---|---|---|---|---|---|---|---|---|
OS X 10.8.0 | 1.6.0_33 | 1.8.2 | MRI 1.8.7 | 0.8.1.dev | 0.4.7 | HTC Desire HD | android-10 | donv |
OS X 10.8.0 | 1.6.0_33 | 1.8.2 | MRI 1.8.7 | 0.8.0 | 0.4.7 | HTC Desire HD | android-10 | donv |
OS X 10.7.3 | 1.6.0_31 | 1.8.2 | MRI 1.8.7 | 0.6.0.rc.3 | 0.4.7 | HTC Desire | android-8 | donv |
OS X 10.7.3 | 1.6.0_31 | 1.8.2 | MRI 1.8.7 | 0.6.0.rc.3 | 0.4.7 | HTC Desire HD | android-10 | donv |
OS X 10.7.3 | 1.6.0_31 | 1.8.2 | MRI 1.8.7 | 0.6.0.rc.3 | 0.4.7 | ASUS TF101 | android-15 | donv |
Connect your device.
ruboto gen app --package org.ruboto.example.spycam
cd spycam
rake update_scripts:restart
You should see the standard "What hath Matz wrought?" screen.
Add a new file src/spycam_server.rb:
require 'monitor'
require 'camera_helper'
require 'ruboto/util/stack'
class SpycamServer
extend MonitorMixin
PORT = 4567
@@server = nil
@@camera_data = nil
def self.start(activity, camera)
Thread.with_large_stack(512) do
synchronize do
if @@server.nil?
activity.server_status = "Loading"
require 'webrick'
activity.server_status = "Loaded"
@@server = WEBrick::HTTPServer.new(:Port => PORT, :DocumentRoot => "#{activity.files_dir.absolute_path}/")
@@server.mount_proc('/') do |req, resp|
case req.path
when '/', 'index.html'
CameraHelper.take_picture(self, camera, activity)
resp.content_type = "text/html"
resp.body = '<html>
<head>
<title>Spycam</title>
</head>
<body>
<a href="/"><img src="latest.jpg"></a>
</body>
</html>'
raise WEBrick::HTTPStatus::OK
when '/latest.jpg'
resp.content_type = "image/jpg"
resp.body = @@camera_data
@@camera_data = nil
raise WEBrick::HTTPStatus::OK
else
resp.body = "Unknown path: #{req.path.inspect}"
raise WEBrick::HTTPStatus::NotFound
end
end
server = @@server
Thread.new{server.start}
end
activity.server_status = "WEBrick started on http://#{get_local_ip_address(activity)}:#{PORT}/"
end
end
end
def self.camera_data=(data)
@@camera_data = data
end
def self.stop
synchronize do
if @@server
@@server.shutdown
sleep 0.1 until @@server.status == :Stop
@@server = nil
end
end
end
private
def self.get_local_ip_address(context)
ip = context.get_system_service(context.class::WIFI_SERVICE).connection_info.ip_address
return "localhost" if ip == 0
[0, 8, 16, 24].map{|n| ((ip >> n) & 0xff).to_s}.join(".")
end
end
Add the following lines to you AndroidManifest.xml file, just after the uses_sdk tag:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
Edit src/spycam_activity.rb to this:
require 'ruboto/activity'
require 'ruboto/widget'
require 'spycam_server'
import android.util.Log
import android.view.Surface
import android.view.WindowManager
ruboto_import_widgets :Button, :LinearLayout, :ScrollView, :TextView
ruboto_import_widget :SurfaceView, "android.view"
class SpycamActivity
def on_create(bundle)
super
rotation = {
Surface::ROTATION_0 => 0,Surface::ROTATION_90 => 90,Surface::ROTATION_180 => 180,Surface::ROTATION_270 => 270
}[window_manager.default_display.rotation]
self.title = "Spycam #{rotation}"
# self.setRequestedOrientation(android.content.pm.ActivityInfo::SCREEN_ORIENTATION_PORTRAIT)
window.add_flags(WindowManager::LayoutParams::FLAG_KEEP_SCREEN_ON)
setContentView(linear_layout(:orientation => :vertical) do
linear_layout do
text_view :text => "Server: "
@server_status_view = text_view
end
linear_layout do
text_view :text => "Picture: "
@camera_status_view = text_view
end
sv = surface_view
@surface_holder_callback = RubotoSurfaceHolderCallback.new(self, rotation)
sv.holder.add_callback @surface_holder_callback
# Deprecated, but still required for older API version
sv.holder.set_type android.view.SurfaceHolder::SURFACE_TYPE_PUSH_BUFFERS
end)
end
def camera_status=(value)
run_on_ui_thread { @camera_status_view.text = value }
end
def server_status=(value)
run_on_ui_thread { @server_status_view.text = value }
end
end
class RubotoSurfaceHolderCallback
def initialize(activity, roation)
@activity = activity
@rotation = roation
end
def surfaceCreated(holder)
puts 'RubotoSurfaceHolderCallback#surfaceCreated'
@camera = android.hardware.Camera.open
parameters = @camera.parameters
parameters.picture_format = android.graphics.PixelFormat::JPEG
parameters.rotation = (360 + (90 - @rotation)) % 360
parameters.set_picture_size(640, 480)
@camera.parameters = parameters
@camera.preview_display = holder
@camera.display_orientation = (360 + (90 - @rotation)) % 360
@camera.start_preview
SpycamServer.start(@activity, @camera)
end
def surfaceChanged(holder, format, width, height)
end
def surfaceDestroyed(holder)
SpycamServer.stop
@camera.stop_preview
@camera.release
@camera = nil
end
end
Add a new file src/camera_helper.rb:
class CameraHelper
def self.take_picture(server, camera, activity)
activity.camera_status = "Set volume..."
am = activity.getSystemService(android.content.Context::AUDIO_SERVICE)
old_volume = am.get_stream_volume(android.media.AudioManager::STREAM_SYSTEM)
am.set_stream_volume(android.media.AudioManager::STREAM_SYSTEM, 0, 0)
activity.camera_status = "Taking picture..."
picture_taken = false
camera.take_picture(nil, nil) do |data, camera|
server.camera_data = String.from_java_bytes(data)
activity.camera_status = "Gotcha!"
camera.start_preview
am.set_stream_volume(android.media.AudioManager::STREAM_SYSTEM, old_volume, 0)
picture_taken = true
end
sleep 0.1 until picture_taken
end
end
Redeploy the application with new scripts
rake update_scripts:restart
The app should restart and show two lines of information:
Server: WEBrick started on http://xxx.xxx.xxx.xxx:4567/
Picture:
Point a web browser to the given URL. You should get a web page with a picture from the camera. Click the camera to update the picture.
Enjoy!