tags:

views:

271

answers:

1

I'm starting with Scala + Android (and using the sbt android plugin). I'm trying to wire a button action to a button without the activity implementing View.OnClickListener.

The button click fails at runtime because the method cannot be found. The document I'm working through says that I need only declare a public void method taking a View on the action, and use that method name in the layout.

What have I done wrong?

MainActivity.scala

package net.badgerhunt.hwa

import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.view.View
import java.util.Date

class MainActivity extends Activity {
  override def onCreate(savedInstanceState: Bundle) = {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main)
  }
  def calculate(button: View): Unit = println("calculating with %s ...".format(button))
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/button"
    android:text=""
    android:onClick="calculate"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"/>

the failure onclick

D/AndroidRuntime(  362): Shutting down VM
W/dalvikvm(  362): threadid=3: thread exiting with uncaught exception (group=0x4001b188)
E/AndroidRuntime(  362): Uncaught handler: thread main exiting due to uncaught exception
E/AndroidRuntime(  362): java.lang.IllegalStateException: Could not find a method calculate(View) in the activity
E/AndroidRuntime(  362):    at android.view.View$1.onClick(View.java:2020)
E/AndroidRuntime(  362):    at android.view.View.performClick(View.java:2364)
E/AndroidRuntime(  362):    at android.view.View.onTouchEvent(View.java:4179)
E/AndroidRuntime(  362):    at android.widget.TextView.onTouchEvent(TextView.java:6540)
E/AndroidRuntime(  362):    at android.view.View.dispatchTouchEvent(View.java:3709)
E/AndroidRuntime(  362):    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
E/AndroidRuntime(  362):    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
E/AndroidRuntime(  362):    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
E/AndroidRuntime(  362):    at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:1659)
E/AndroidRuntime(  362):    at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1107)
E/AndroidRuntime(  362):    at android.app.Activity.dispatchTouchEvent(Activity.java:2061)
E/AndroidRuntime(  362):    at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:1643)
E/AndroidRuntime(  362):    at android.view.ViewRoot.handleMessage(ViewRoot.java:1691)
E/AndroidRuntime(  362):    at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(  362):    at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(  362):    at android.app.ActivityThread.main(ActivityThread.java:4363)
E/AndroidRuntime(  362):    at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(  362):    at java.lang.reflect.Method.invoke(Method.java:521)
E/AndroidRuntime(  362):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
E/AndroidRuntime(  362):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
E/AndroidRuntime(  362):    at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime(  362): Caused by: java.lang.NoSuchMethodException: calculate
E/AndroidRuntime(  362):    at java.lang.ClassCache.findMethodByName(ClassCache.java:308)
E/AndroidRuntime(  362):    at java.lang.Class.getMethod(Class.java:1014)
E/AndroidRuntime(  362):    at android.view.View$1.onClick(View.java:2017)
E/AndroidRuntime(  362):    ... 20 more

UPDATE

Thinking that this may have been an error with the sbt android plugin I made doubly sure that the method was present after compilation. Using javap ...

Compiled from "MainActivity.scala"
public class net.badgerhunt.hwa.MainActivity extends android.app.Activity implements scala.ScalaObject{
    public net.badgerhunt.hwa.MainActivity();
    public void calculate(android.view.View);
    public void onCreate(android.os.Bundle);
    public int $tag()       throws java.rmi.RemoteException;
}
+2  A: 

The sbt android plugin includes a ProGuard task that strips out all unused code. Very cool stuff and helps to really slim down the resulting .apk file but unfortunately callback methods aren't usually referenced in your code so, by defalt, ProGuard will throw them out. To see it yourself try using javap on the MainActivity class but set the classpath to target/your_scala_version/classes.min.jar. You need to tell proguard what to explicitly keep. There is already a set of -keep options in the default setup of sbt android plugin but this is specific to your project so you will have to edit your project definition in project/build/YourProjectName.scala. Look at the code of sbt android plugin and lok for proguardTask definition. You will have to override that and add your additional -keep options. This is what I did:

import sbt._
import java.io._
import proguard.{Configuration=>ProGuardConfiguration, ProGuard, ConfigurationParser}
import sbt._
import Process._

trait Defaults {
  def androidPlatformName = "android-1.6"
}
class TestAndro2(info: ProjectInfo) extends ParentProject(info) {
  override def shouldCheckOutputDirectories = false
  override def updateAction = task { None }

  lazy val main  = project(".", "testAndro2", new MainProject(_))

  class MainProject(info: ProjectInfo) extends AndroidProject(info) with Defaults {
    val scalatest = "org.scalatest" % "scalatest" % "1.0" % "test"
    override def proguardTask = task { 
      val args = "-injars" ::  mainCompilePath.absolutePath+File.pathSeparator+
      scalaLibraryJar.getAbsolutePath+"(!META-INF/MANIFEST.MF,!library.properties)"+
      (if (!proguardInJars.getPaths.isEmpty) File.pathSeparator+proguardInJars.getPaths.map(_+"(!META-INF/MANIFEST.MF)").mkString(File.pathSeparator) else "") ::                             
        "-outjars" :: classesMinJarPath.absolutePath ::
        "-libraryjars" :: libraryJarPath.getPaths.mkString(File.pathSeparator) :: 
        "-dontwarn" :: "-dontoptimize" :: "-dontobfuscate" :: 
        "-dontwarn" :: "-dontoptimize" :: "-dontobfuscate" :: "-printseeds" ::
        """-keep public class com.test.android.MainActivity {
          public void calculate(android.view.View);
        }""" ::
        "-keep public class * extends android.app.Activity" ::
        "-keep public class * extends android.app.Service" ::
        "-keep public class * extends android.appwidget.AppWidgetProvider" ::
        "-keep public class * implements junit.framework.Test { public void test*(); }" :: proguardOption :: Nil

        val config = new ProGuardConfiguration
        new ConfigurationParser(args.toArray[String], info.projectPath.asFile).parse(config)    
        new ProGuard(config).execute
        None
      }
    }
  }

Essentially, I added the -printseeds and one -keep option to keep the calculate() method of the MainActivity. -printseeds is good for debugging as it tells proguard to print the names of classes and methods that have been kept. ProGuard has a huge set of configuration options and you will need to keep an eye on them when building your project as there are lots of ambiguous situations where ProGuard won't do the right thing by default.

IgorR