Ruboto – Flickr Photo Search

A little more trial on Ruboto. Initially, I just wanted to create a sample app to use ListView which displays some information acquired from certain online service (ex. twitter). However, many of the online APIs use SSL and/or OAuth kind of encryptions/authentications, and it doesn’t work well with Ruboto. After struggles, I could finally make it work with simple flickr API call. It provides nice and simple API, and I didn’t have to use standard ruby-gem (which I couldn’t use with Ruboto). Anyway, I could learn some Android’s GUI layout defined using XML, and some API calls like AsyncTask. It was a good exercise.

Ruboto is an interesting framework with powerful JRuby features. Though it has limitation in performance, many of the Android API works fine by just importing Java library through “java_import” keyword of JRuby. It’s a little tough to track the trace of “adb logcat” results, as it involves mixtures of Java/Ruby stack traces. But many of the cases, it seems to provide enough information for troubleshooting.

The following is some excerpt from my trial. Entire code is in my GitHub repo (https://github.com/parroty/ruboto_flickr).

flickr1

flickr2

ruboto_flickr_activity.rb

require 'ruboto/widget'
require "ruboto/util/stack"
with_large_stack {
  require 'flickr_reader'
}

java_import "android.widget.ArrayAdapter"
java_import "android.widget.ListView"
java_import "android.graphics.drawable.Drawable"
java_import "android.os.AsyncTask"
java_import "android.app.ProgressDialog"
java_import "android.content.Context"
java_import "android.content.DialogInterface"
java_import "android.util.Log"
java_import "java.net.URL"

ruboto_import_widgets :Button, :LinearLayout, :TextView

IMAGE_PER_PAGE = 10

class RubotoFlickrActivity
  def on_create(bundle)
    super
    setTitle 'Flickr Searcher'
    self.setContentView(Ruboto::R::layout::activity_main)

    view = findViewById(Ruboto::R::id::list_view)
    view.setAdapter(IconicAdapter.new(self, []))
    view.setScrollingCacheEnabled(false)

    btn = findViewById(Ruboto::R::id::search_button)
    btn.setOnClickListener(MyOnClickListner.new(self))
  end

  def update_list_view(search_word)
    ImageCache.clear
    view = findViewById(Ruboto::R::id::list_view)
    task = SearchTask.new(self, view, search_word)
    task.execute
  end
end

class MyOnClickListner
  def initialize(activity)
    @activity = activity
  end

  def onClick(view)
    text_view = @activity.findViewById(Ruboto::R::id::search_text)
    @activity.update_list_view("#{text_view.text}")
  end
end

class IconicAdapter < ArrayAdapter
  def initialize(activity, items)
    @activity = activity
    @items    = items

    super(@activity, Ruboto::R::layout::row, Ruboto::R::id::title, @items.map(&:headline))
  end

  def getView(position, convert_view, parent)
    row  = super
    item = @items[position]

    view = row.findViewById(Ruboto::R::id::icon)
    task = ImageLoadTask.new(@activity, self, item, view)
    task.execute

    content_view = row.findViewById(Ruboto::R::id::content)
    content = "#{item.info.description}\n\n#{item.info.url}"
    content_view.setText(content)

    row
  end

  def fetch_image(address)
    input_stream = URL.new(address).getContent
    drawable = Drawable.createFromStream(input_stream, "")
    input_stream.close

    drawable
  end
end

class SearchTask < AsyncTask
  PROGRESS_MAX = 100

  def initialize(context, view, search_word)
    super()
    @context = context
    @view = view
    @search_word = search_word
  end

  def onPreExecute
    @dialog = ProgressDialog.new(@context)
    @dialog.setTitle("Please wait for images to load")
    @dialog.setMessage("Searching flickr ...")
    @dialog.setProgressStyle(ProgressDialog::STYLE_HORIZONTAL)
    @dialog.setCancelable(false)
    @dialog.setMax(PROGRESS_MAX)
    @dialog.setProgress(0)
    @dialog.show
  end

  def onProgressUpdate(values)
    @dialog.setProgress(values.first)
  end

  def doInBackground(param)
    with_large_stack {
      reader = FlickrReader.new
      reader.search(:tag => @search_word, :per_page => IMAGE_PER_PAGE) do |index|
        publishProgress((index + 1) * (PROGRESS_MAX / IMAGE_PER_PAGE))
      end
    }
  end

  def onPostExecute(items)
    @dialog.dismiss
    @view.setAdapter(IconicAdapter.new(@context, items))
  end
end

class ImageLoadTask < AsyncTask
  def initialize(activity, adapter, item, view)
    super()
    @activity = activity
    @adapter  = adapter
    @item     = item
    @view     = view
  end

  def doInBackground(param)
    url = @item.small_image_url
    ImageCache.get(url) || ImageCache.put(url, @adapter.fetch_image(url))
  end

  def onPostExecute(param)
    @view.setImageDrawable(param)
  end
end

class ImageCache
  @@image_hash = {}

  def self.put(key, image)
    @@image_hash[key] = image
  end

  def self.get(key)
    @@image_hash[key]
  end

  def self.clear
    @@image_hash = {}
  end
end

flickr_reader.rb

require 'open-uri'
require 'rexml/document'
require 'cgi'

class FlickrReader
  def search(options)
    photos_xml = API.search(options[:tag], options[:per_page])

    index = 0
    photos = Parser.parse_photos_search(photos_xml)
    photos.each do |photo|
      info_xml = API.get_info(photo.id)
      photo.info = Parser.parse_photos_get_info(info_xml)
      yield index if block_given?
      index += 1
    end
    photos
  end
end

class API
  FLICKR_API_URL = "http://www.flickr.com/services/rest/?api_key=%s&method=%s&%s"
  ATTRIBUTION_LICENSE = '4'

  def self.search(tag, per_page = 10)
    exec('flickr.photos.search', 'tags' => tag, 'license' => ATTRIBUTION_LICENSE, 'per_page' => per_page.to_s)
  end

  def self.get_info(photo_id)
    exec('flickr.photos.getInfo', 'photo_id' => photo_id.to_s)
  end

private
  def self.exec(method_name, arg_map = {}.freeze)
    args = arg_map.collect do |k,v|
      CGI.escape(k) << '=' << CGI.escape(v)
    end.join('&')

    if ENV['FLICKR_API_KEY']
      api_key = ENV['FLICKR_API_KEY']
    else
      require 'flickr_api_key'
      api_key = FLICKR_API_KEY
    end

    url = FLICKR_API_URL % [api_key, method_name, args]
    REXML::Document.new(open(url).read)
  end
end

class Parser
  def self.parse_photos_search(xml)
    list = []
    REXML::XPath.each(xml, '//photo') do |elem|
      photo = Photo.new
      photo.server = elem.attribute('server').to_s
      photo.id     = elem.attribute('id').to_s
      photo.secret = elem.attribute('secret').to_s
      photo.title  = elem.attribute('title').to_s

      list << photo
    end
    list
  end

  def self.parse_photos_get_info(xml)
    info = PhotoInfo.new
    info.description = REXML::XPath.first(xml, '//description').text || ""
    info.url         = REXML::XPath.first(xml, '//url').text || ""
    info.owner       = REXML::XPath.first(xml, '//owner').attribute('username').to_s
    info
  end
end

class Photo
  attr_accessor :server, :id, :secret, :title, :info

  def small_image_url
    "http://static.flickr.com/#{@server}/#{@id}_#{@secret}_m.jpg"
  end

  def info=(info)
    @info = info
  end

  def headline
    if @info && @info.owner
      "#{title} by #{@info.owner}"
    else
      title
    end
  end

  def to_s
    headline
  end
end

class PhotoInfo
  attr_accessor :description, :url, :owner
end
Advertisements

Posted on April 10, 2013, in Ruby. Bookmark the permalink. 2 Comments.

  1. Nice writeup! I think the file name for the second source file is wrong :)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: