Talk: SF Bay Area JRuby Meetup (September 22, 2011)

rscottm edited this page Oct 5, 2011 · 38 revisions
Clone this wiki locally

Talk for the SF Bay Area JRuby Meetup

Introduction to Ruboto and Android

  • 3 ways to do Ruby on Android

    • Rhodes: Open source Ruby-based framework for cross-platform app development
    • Scripting Layer 4 Android: Simplified API for various scripting languages (including JRuby)
    • Ruboto: Goal - Make JRuby a first-class development language for Android
  • Basics of Android

    • Linux kernel, Dalvik VM
    • Activity - Visual component, roughly one screen
    • Service - Background process
    • Others - BroadcastReceiver and ContentProvider
    • Intent - "I want to do something" abstraction layer
    • Manifest - Informs system about activities, services, permissions, etc.
  • Challenges

    • Mobile hardware limitations
    • Dalvik VM - different byte code, stack limits
    • Passing control for callbacks to scripts
    • Excess components of JRuby
    • Android resource framework
  • Basics of Ruboto

    • Ruboto IRB - App for running scripts
    • Ruboto Core - Generates apps (set up callbacks)
    • Shared
      • Core package shared on device
      • ruboto.rb - Device script encapsulating JRuby/Android intaraction

Demo, Demo, Demo

  • Ruboto IRB

    • Webrick server (demos use of stdlib, notification, service)

      • Standard demo script to obtain remote access: demo-irb-server.rb

      • Extend demo to get access to demo scripts. Note: this assumes a subdirectory called "demo" that I added to contain the demo scripts, also uses SyntaxHighlighter 3.0.83.

        if $server
          class DemoListServlet < HTTPServlet::AbstractServlet
            def list_page
              %Q[
                <html>
                  <body>
                    <h2>Demos</h2>
                    #{yield}
                  </body>
                </html>
              ]
            end
        
            def do_GET(req, resp)
              resp.content_type = "text/html"
              resp.body = list_page do
                Dir.glob("demo/*.rb").sort.map do |i|
                  unless File.directory?(i)
                    $server.mount("/#{i}", DemoServlet, i)
                    "<a href='/#{i}'>#{i.split('.')[1].split('-').map(&:capitalize).join(' ')}</a><br/>"
                  end
                end.join
              end
            end
        
            def do_POST(req, resp)
              do_GET(req, resp)
            end
          end
        
          class DemoServlet < HTTPServlet::AbstractServlet
            def body(name="untitled.rb", script="")
              %Q[
                <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
                <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
                  <head>
                    <script type="text/javascript" src="/demo/shCore.js"></script>
                    <script type="text/javascript" src="/demo/shBrushRuby.js"></script>
                    <link type="text/css" rel="stylesheet" href="/demo/shCoreDefault.css"/>
                    <script type="text/javascript">SyntaxHighlighter.all();</script>
                  </head>
                  <body style="background: white; font-family: Helvetica">
                    <h1>#{name.split('.')[1].split('-').map(&:capitalize).join(' ')}</h1>
        
                    <pre class="brush: ruby; toolbar: false; auto-links: false #{name.split('.').length > 3 ? ('; highlight: ' + name.split('.')[2].split('-').map(&:to_i).inspect) : ''}">#{HTMLUtils.escape(script)}</pre>
                    <form action='/' method='post'>
                      <input type='submit' value='Eval' />
                      <br/>
                      <textarea id='code' name='code' rows='1' cols='1'>#{HTMLUtils.escape(script)}</textarea>
                    </form>
                  </body>
                </html>
              ]
            end
        
            def do_GET(req, resp)
              resp.content_type = "text/html"
              name = @options[0] || "untitled.rb"
              script = File.exists?(name) ? IO.read(name) : ""
              resp.body = body(name, script)
            end
          end
        
          $server.mount("/demos", DemoListServlet, nil)
          puts "Demos mounted: http://#{ip_address}:#{SERVER_PORT}/demos"
        else
          puts "Mount failed: No server running"
        end
    • Intents: Opening 3rd-party apps

      • Camera

        $activity.startActivity(
            Java::android.content.Intent.new(
                Java::android.provider.MediaStore::ACTION_IMAGE_CAPTURE))
      • Browser

        java_import "android.content.Intent"
        java_import "android.net.Uri"
        
        $activity.startActivity(Intent.new(Intent::ACTION_VIEW, Uri.parse("http://ruboto.org")))
      • Map

        java_import "android.content.Intent"
        java_import "android.net.Uri"
        
        $activity.startActivity(
          Intent.new(
            Intent::ACTION_VIEW, 
            Uri.parse("geo:0,0?q=engine+yard+san+francisco")
          )
        )
    • Activity:

      • Domo Arigato, Mr. Ruboto (basic ruboto activity)

        require 'ruboto/activity'
        
        java_import "android.view.Gravity"
        java_import "android.widget.TextView"
        
        $activity.start_ruboto_activity "$hello_world" do
          setTitle "Domo Arigato"
        
          def on_create(bundle)
            tv = TextView.new(self)
            tv.text = "Domo Arigato"
            tv.text_size = 64
            tv.gravity = (Gravity::CENTER_HORIZONTAL | Gravity::CENTER_VERTICAL)
            tv.on_click_listener = proc{|view| view.text = "Mr. Ruboto"}
        
            self.content_view = tv
          end
        end
      • Domo Arigato, Mr. Ruboto version 2 (use of widgets)

        require 'ruboto/activity'
        require 'ruboto/widget'
        
        java_import "android.view.Gravity"
        
        ruboto_import_widgets :TextView, :LinearLayout
        
        $activity.start_ruboto_activity "$hello_world" do
          setTitle "Domo Arigato"
        
          def domo_text_view
            text_view:layout => {:width= => :fill_parent, :height= => :fill_parent, :weight= => 1.0},
                      :text => "Domo Arigato",
                      :text_size => 48,
                      :gravity => (Gravity::CENTER_HORIZONTAL | Gravity::CENTER_VERTICAL),
                      :on_click_listener => (proc do |view| 
                        view.text = ("Mr. Ruboto" == view.text ? "Domo Arigato" : "Mr. Ruboto")
                      end)
          end
        
          def on_create(bundle)
            self.content_view =
              linear_layout(:orientation => :vertical) do
                linear_layout(:layout => {:width= => :fill_parent, :height= => :fill_parent, :weight= => 1.0}) do
                  domo_text_view
                  domo_text_view
                end
                linear_layout(:layout => {:width= => :fill_parent, :height= => :fill_parent, :weight= => 1.0}) do
                  domo_text_view
                  domo_text_view
                end
              end
          end
        end
      • Camera/Picture (implements interface), standard demo-camera.rb

        • Adding camera click mount to server

          if $server
            $server.mount_proc('/click') do |req, resp| 
              f = $activity.take_picture
              sleep 2 # wait for picture to be written to the file 
              resp.content_type = "image/jpg"
              resp.body = IO.read f
            end
          end
      • Open GL (implements interface, shows open GL) demo-opengl.rb

    • Accessing device features

      • Sensors

        java_import "android.content.Intent"
        java_import "android.net.Uri"
        java_import "android.location.LocationManager"
        java_import "android.content.Context"
        
        l = $activity.getSystemService(Context::LOCATION_SERVICE).
                         getLastKnownLocation(LocationManager::NETWORK_PROVIDER)
        
        $activity.startActivity(
          Intent.new(
            Intent::ACTION_VIEW, 
            Uri.parse("geo:#{l.latitude},#{l.longitude}")
          )
        )
      • Voice input

        require 'ruboto/activity'
        require 'ruboto/util/toast'
        
        java_import "android.speech.RecognizerIntent"
        java_import "android.content.Intent"
        java_import "android.net.Uri"
        
        $activity.start_ruboto_activity "$speech_demo" do
          intent = Intent.new(RecognizerIntent::ACTION_RECOGNIZE_SPEECH)
          intent.putExtra(RecognizerIntent::EXTRA_LANGUAGE_MODEL,
                          RecognizerIntent::LANGUAGE_MODEL_FREE_FORM)
          intent.putExtra(RecognizerIntent::EXTRA_PROMPT, "Speech recognition demo")
          startActivityForResult(intent, 1)
        
          def on_activity_result(requestCode, resultCode, data)
            if resultCode == Activity::RESULT_OK
              results = data.get_string_array_list_extra(RecognizerIntent::EXTRA_RESULTS)
              puts results.join("\n")
              startActivity(Intent.new(Intent::ACTION_VIEW, 
                  Uri.parse("geo:0,0?q=" + results[0].gsub(" ", "+"))))
            end
            finish
          end
        end
    • Multiple Activities: "One Script to Rule Them All" (create a script to list and run our demos)

      require "ruboto.rb"
      java_import "android.app.AlertDialog"
      
      $main_binding = self.instance_eval{binding}
      
      def launch_list(context, title, list, &block)
        ruboto_import_widgets :ListView
      
        context.start_ruboto_activity "$list" do
          setTitle title
          @list = list
          @block = block
          def on_create(bundle)
              self.content_view = list_view :list => @list, 
                                    :on_item_click_listener => proc{|av, v, p, i| @block.call(@list, p)}
          end
        end
      end
      
      def show_demo(context, file, name)
        java_import "android.view.Gravity"
        ruboto_import_widgets :EditText, :LinearLayout, :Button
      
        context.start_ruboto_activity "$demo" do
          getWindow.setSoftInputMode(
               android.view.WindowManager::LayoutParams::SOFT_INPUT_STATE_VISIBLE | 
               android.view.WindowManager::LayoutParams::SOFT_INPUT_ADJUST_RESIZE)
      
          setTitle name
          @file = file
          def on_create(bundle)
              self.content_view = 
                  linear_layout(:orientation => :vertical) do
                      button :text => "Eval", :layout => {:width= => :fill_parent},
                             :on_click_listener => proc{eval_code @tv.text.to_s}
                      @tv = edit_text :layout => {:height= => :fill_parent},
                                      :text => load_demo(@file),
                                      :gravity => Gravity::TOP, 
                                      :horizontally_scrolling => true,
                                      :text_size => 24
                  end
          end
        end
      end
      
      def eval_code(code)
        begin
          $main_binding.eval(code)
        rescue => e
            AlertDialog::Builder.new($activity).
              setTitle("Error").
              setMessage(e.backtrace.join("\n")).
              setPositiveButton("Ok", nil).
              create.
              show
        end
      end
      
      def load_demo(name)
          rv = ""
      
          if File.exists?(name)
              rv = IO.read(name)
          else
              buff_size = 0x2000
              buff = Java::byte[buff_size].new
      
              i = $activity.getAssets.open(name)
              i = java.io.BufferedInputStream.new(i, buff_size)
              x = i.read(buff, 0, buff_size)
      
              while x != -1
                rv += String.from_java_bytes(x == buff_size ? buff : buff[0..(x-1)])
                x = i.read(buff, 0, buff_size)
              end
          end
      
          rv
      end
      
      # Check file system or read from assets
      demo_files = nil
      if File.directory?("demo")
        demo_files = Dir.glob("demo/*.rb").sort
      else
        demo_files = $activity.getAssets.list("demo").select{|i| i[-3..-1] == ".rb"}.sort.map{|i| "demo/#{i}"}
      end
      demo_names = demo_files.map{|i| i.split('.')[1].split('-').map(&:capitalize).join(' ')}
      
      launch_list($activity, "Please pick a demo", demo_names) do |list, pos|
        show_demo $list, demo_files[pos], demo_names[pos]
      end
  • Ruboto Core

    • Development requirements

      • JDK & Ant
      • Ruby, Rubygems, Rake
      • Android SDK & Platforms
      • Gems: ruboto-core & jruby-jars
      • Path: JDK, Ant, Android, Gems bin
    • Note: To match the version of ruboto.rb in Ruboto IRB v0.6 I have created a fork of ruboto-core. Use this gem.

    • Generating the basic app

      ruboto gen app --package org.rubyandroid.demo.jruby_meetup_demo --target=android-12  --min-sdk=android-7
      cd jruby_meetup_demo 
      rake debug
      
    • Modify to hold our demo app

      • Copy demo scripts to assets

      • Add About Dialog script as first demo

        require 'ruboto/activity'
        require 'ruboto/widget'
        
        ruboto_import_widgets :ScrollView, :TextView
        
        $activity.start_ruboto_dialog "$about" do
          setTitle "About The JRuby Meetup Demo"
        
          def on_create(bundle)
            self.content_view =
              scroll_view do
                tv = $activity.text_view :padding => [5,5,5,5],
                                         :text => @message,
                                         :text_size => 18,
                                         :text_color => Java::android.graphics.Color::WHITE
                Java::android.text.util::Linkify.addLinks(tv, Java::android.text.util::Linkify::ALL)
              end
          end
        
        @message = %Q[This is a demo of Ruboto (JRuby on Android) created by Scott Moyer for the SF Bay Area JRuby Meetup (September 22, 2011). It consists of a series of demo scripts that you can view and run. For more information:
        
        Wiki of demo:
        https://github.com/ruboto/ruboto-core/wiki/Talk%3A-SF-Bay-Area-JRuby-Meetup-%28September-22%2C-2011%29
        
        Meetup:
        http://www.meetup.com/SF-Bay-Area-JRuby-Meetup
        
        Ruboto Community:
        http://www.ruboto.org
        
        JRuby:
        http://jruby.org
        
        Android Development:
        http://developer.android.com]
        end
      • Generate callback interfaces

        ruboto gen interface android.view.SurfaceHolder.Callback --name RubotoSurfaceHolderCallback
        ruboto gen interface android.opengl.GLSurfaceView.Renderer --name RubotoGLSurfaceViewRenderer
        
      • Request permissions through AndroidManifes.xml

        <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
        <uses-permission android:name="android.permission.CAMERA"/>
      • Tell the activities to manage their own orientation changes in AndroidManifes.xml

        android:configChanges="orientation"
        
    • Create keystore to sign the app for release onto the Market

      keytool -genkey -v -keystore meetup.keystore  -alias meetup -keyalg RSA -keysize 2048 -validity 10000
      
    • Create a build.properties file to specify the keystore for signing

      key.store=meetup.keystore
      key.alias=meetup
      
    • Build release

      rake release
      
    • Getting to Market

      • Requires a market account (one time $25)
      • Will need:
        • Screen shots (can use emulator)
        • 512x512 image
        • Description
    • Here's our app

Where From Here?

  • Key outstanding issues
    • Start up speed
    • Stack limitations
    • Size (managed some now)
    • Compiling ruby
    • Generating byte code on the device
    • Handling callbacks from libraries outside of core Android
  • Links
  • Getting involved
    • Use it and let us know what is needed
    • Create demo scripts/tutorials (features, gems)
    • Work on the outstanding problems